我是否可以在使用C++构建静态本地时访问它?

kin*_*nak 11 c++ static-initialization visual-studio-2015

保证在C++标准首次使用时实例化静态本地.但是,我想知道如果我在构建时访问静态本地对象会发生什么.我认为这是UB.但在以下情况下避免这种情况的最佳做法是什么?

一个问题的情况

Meyers Singleton模式在静态getInstance()方法中使用静态局部来在第一次使用时构造对象.现在,如果构造函数(直接或indireclty)getInstance()再次调用,我们将面临静态初始化尚未完成的情况.这是一个最小的例子,它说明了问题情况:

class StaticLocal {
private:
    StaticLocal() {
        // Indirectly calls getInstance()
        parseConfig();
    }
    StaticLocal(const StaticLocal&) = delete;
    StaticLocal &operator=(const StaticLocal &) = delete;

    void parseConfig() {
        int d = StaticLocal::getInstance()->getData();
    }
    int getData() {
        return 1;
    }

public:
    static StaticLocal *getInstance() {
        static StaticLocal inst_;
        return &inst_;
    }

    void doIt() {};
};

int main()
{
    StaticLocal::getInstance()->doIt();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在VS2010中,这没有问题,但VS2015死锁.

对于这种简单,减少的情况,显而易见的解决方案是直接呼叫getData(),而无需getInstance()再次呼叫.但是,在更复杂的情况下(根据我的实际情况),这种解决方案是不可行的.

尝试解决方案

如果我们改变getInstance()方法来处理像这样的静态局部指针(从而放弃Meyers Singleton模式):

static StaticLocal *getInstance() {
    static StaticLocal *inst_ = nullptr;
    if (!inst_) inst_ = new StaticLocal;
    return inst_;
}
Run Code Online (Sandbox Code Playgroud)

很明显,我们得到了无休止的递归.inst_nullptr第一次调用,所以我们用构造函数调用new StaticLocal.此时,inst_仍然nullptr只会在构造函数完成时分配.但是,构造函数将getInstance()再次调用,查找nullptrin inst_,从而再次调用构造函数.又一次,......

一种可能的解决方案是将构造函数的主体移动到getInstance():

StaticLocal() { /* do nothing */ }

static StaticLocal *getInstance() {
    static StaticLocal *inst_ = nullptr;
    if (!inst_) {
        inst_ = new StaticLocal;
        inst_->parseConfig();
    }
    return inst_;
}
Run Code Online (Sandbox Code Playgroud)

这会奏效.但是,我对这种情况不满意,因为构造函数应该构造一个完整的对象.如果这种情况可以成为例外,那就是一个单身人士,这是有争议的.但是,我不喜欢它.

但更重要的是,如果班级有一个非平凡的析构函数呢?

~StaticLocal() { /* Important Cleanup */ }
Run Code Online (Sandbox Code Playgroud)

在上面的情况下,从不调用析构函数.我们松散了RAII,因此是C++的一个重要区别特征!我们处在像Java或C#这样的世界......

所以我们可以将我们的单例包装在某种智能指针中:

static StaticLocal *getInstance() {
    static std::unique_ptr<StaticLocal> inst_;
    if (!inst_) {
        inst_.reset(new StaticLocal);
        inst_->parseConfig();
    }
    return inst_.get();
}
Run Code Online (Sandbox Code Playgroud)

这将在程序退出时正确调用析构函数.但它迫使我们公开析构函数.

在这一点上,我觉得我正在做编译器的工作......

回到原来的问题

这种情况真的是未定义的行为吗?或者它是VS2015中的编译器错误?

什么是这种情况的最佳解决方案,可以放弃完整的构造函数和RAII?

iva*_*ult 14

这导致c ++ 11标准的未定义行为.相关部分是6.7:

如果控制在初始化变量时同时进入声明,则并发执行应等待初始化完成.如果控件在初始化变量时以递归方式重新输入声明,则行为未定义.

标准的例子如下:

int foo(int i) {
    static int s = foo(2*i); // recursive call - undefined
    return i+1;
}
Run Code Online (Sandbox Code Playgroud)

您正面临死锁,因为MSVC插入互斥锁定/解锁以使静态变量初始化线程安全.一旦你以递归方式调用它,你就会在同一个线程中锁定相同的互斥锁两次,导致死锁.

就是llvm编译器内部实现静态初始化的方法.

IMO的最佳解决方案是根本不使用单身人士.重要的开发人员倾向于认为单身人士是反模式的.你提到的问题很难调试,因为它发生在main之前.因为全局变量初始化的顺序是未定义的.此外,可能涉及多个转换单元,因此编译器不会捕获此类错误.因此,当我在生产代码中遇到同样的问题时,我不得不删除所有的单身人士.

如果您仍然认为单例是正确的方法,那么当您的单例对象拥有(例如,将它们保存为成员)所有GetInstance在单例初始化期间调用的类时,您需要以某种方式重新构建代码.把你的类想象为所有权树,其中单例是根.当您创建子项时,如果子项需要,则将引用传递给父项.


小智 7

问题是在类中,你应该使用"this"而不是调用getInstance,特别是:

void parseConfig() {
    int d = StaticLocal::getInstance()->getData();
}
Run Code Online (Sandbox Code Playgroud)

应该只是:

void parseConfig() {
    int d = getData();
}
Run Code Online (Sandbox Code Playgroud)

该对象是单例,因为构造函数是私有的,因此用户无法构造任意数量的对象.假设只有一个对象的实例,编写整个类是不好的设计.在某些时候,有人可能会像这样延伸单身人士的概念:

static StaticLocal *getInstance(int idx) {
    static StaticLocal inst_[3];
    if (idx < 0 || idx >= 3)
      throw // some error;
    return &inst_[idx];
}
Run Code Online (Sandbox Code Playgroud)

当发生这种情况时,如果在整个类中没有调用getInstance(),则更新代码会容易得多.

为什么会发生这样的变化?想象一下,你在20年前写了一个代表CPU的课程.当然系统中只有一个CPU,所以你把它变成一个单独的.然后,突然间,多核系统变得司空见惯.您仍然只需要与系统中的核心一样多的CPU类实例,但在程序运行之前您不会知道在给定系统上实际有多少核心.

故事的道德:使用this指针不仅避免了递归调用getInstance(),而且还可以证明你的代码.


iam*_*ind 2

实际上,当前形式的这段代码陷入了3 路无限递归。因此它永远不会起作用。

getInstance() --> StaticLocal()
 ^                    |  
 |                    |  
 ----parseConfig() <---
Run Code Online (Sandbox Code Playgroud)

为了让它发挥作用,以上三种方法中的任何一种都必须妥协并走出恶性循环。你判断得对,parseConfig()是最好的人选。

假设构造函数中的所有递归内容都放入构造函数中parseConfig(),非递归内容保留在构造函数中。然后你可以执行以下操作(仅相关代码):

    static StaticLocal *s_inst_ /* = nullptr */;  // <--- introduce a pointer

public:
    static StaticLocal *getInstance() {
      if(s_inst_ == nullptr)
      {   
        static StaticLocal inst_;  // <--- RAII
        s_inst_ = &inst_;  // <--- never `delete s_inst_`!
        s_inst_->parseConfig();  // <--- moved from constructor to here
      }   
      return s_inst_;
    }   
Run Code Online (Sandbox Code Playgroud)

这很好用。