C+中的静态初始化顺序+; < C++ >静态初始化顺序问题的一个常见的解决方法是“先用构造”成语。这个习惯用法在静态对象周围放置一个函数包装器

C+中的静态初始化顺序+; < C++ >静态初始化顺序问题的一个常见的解决方法是“先用构造”成语。这个习惯用法在静态对象周围放置一个函数包装器,c++,C++,如果没有这个成语,你会: Foo bar; Foo &bar() { static Foo *ptr = new Foo(); return *ptr; } 有了这个成语,你就有了: Foo bar; Foo &bar() { static Foo *ptr = new Foo(); return *ptr; } 从第一个移动到第二个需要bar的所有用法从bar更改为bar()。我现在的处境是,我无法做出这样的改变(太多的使用站点,失去了操作符的自

如果没有这个成语,你会:

Foo bar;
Foo &bar()
{
   static Foo *ptr = new Foo();
   return *ptr;
}
有了这个成语,你就有了:

Foo bar;
Foo &bar()
{
   static Foo *ptr = new Foo();
   return *ptr;
}
从第一个移动到第二个需要
bar
的所有用法从
bar
更改为
bar()
。我现在的处境是,我无法做出这样的改变(太多的使用站点,失去了
操作符的自然使用性,你就不能:

Foo& bar = bar();
在什么地方继续使用酒吧

还有,为什么不把这个习惯用法实现为:

Foo& bar()
{
    static Foo foo;
    return foo;
}

它还远远不够完美,但您可以随时按照 iostream do确保初始化
std::cin
std::cout
。 (这有时被称为俏皮的反成语,或“施瓦兹” 计数器,在技术发明者之后。)有几个 变体,但基本思想取决于 在单个翻译单元内保证初始化,因此如果 在标题中定义某个特殊类型的(静态)实例,它将 be(通常情况下,由于顶部包含标题)在 源文件中的任何内容。此静态 实例检查全局标志或计数器;如果值为0,则 初始化全局对象,并递增计数器,以便 以下构造函数不会初始化它。(没有顺序。) 计数器的初始化问题,因为它依赖于零 唯一的问题是如何声明对象本身。 我认为在最早的版本中,它是在汇编程序中声明的,作为 数组中有足够的字节。我发现可以工作(尽管不能保证) 是声明一个特殊的、无操作的构造函数,并调用 在变量的“初始化”中,当然还有 初始化对象使用新放置

举个简单的例子,这一点可能更清楚:

Foo.hh:

class Foo
{
    enum Hidden { noop };
    Foo( Hidden ) {}
public:
    Foo();    //  The real constructor.

    static Foo bar;

    class FoobarInitializer
    {
    public:
        FoobarInitializer();
    }
};

static FoobarInitializer initializeFoobar;
Foo.cc:

namespace {

int initCount;
}

Foo Foo::bar( Foo::noop );

Foo::FoobarInitializer::FoobarInitializer()
{
    if ( initCount == 0 ) {
        new (&Foo::bar) Foo();
    }
    ++ initCount;
}
如果
bar
不是
Foo
的成员,但 你需要把更多的事情公之于众 朋友,但至少,
Foo::noop
必须是公共的。)

我要重复一遍,这不是保证的:
Foo::Foo(noop)
可能是 在初始化类构造之后,在
bar
上调用,并且 允许实现在输入前在内存上涂鸦 构造器的主体。但它在实践中一直对我有效,
我在许多不同的编译器中使用过它。

您可以将Foo重命名为类似FooImpl的名称,保留“第一次使用时构造”的习惯用法。 然后:


有了这个包装器,您的其余代码就不需要更改了。

James Kanze解决方案的更简单的变体

但由于同样的原因失败了:

福安 Foo.cpp 现在,在每个包含Foo.h的文件中,我们都引入了一个匿名名称空间来初始化一个本地对象bar(即引用)。因此,在使用bar之前,它会被初始化(因为您必须包含Foo.h才能知道要使用它们的Foo对象)

此(和James Kanze)解决方案可能失败的原因是:

您需要进行一定程度的间接操作才能使其失败:

酒吧 Bar.cpp 呜呜,呜呜 fooooo.cpp 简单的解决办法是: 假设您可以重新编译所有代码。
但如果你不能做到这一点,这里的大多数解决方案都将失败

假设您的旧Foo看起来像这样: 老富

extern Foo  bar;
老富

#include "Foo.h"
Foo& bar;
然后,您可以将其替换为: 新富

Foo& getBar();

// Not like I like this but it would be better to force users to change
// how they use bar() rather than use this hack.
#define   bar    getBar()
新Foo.cpp

#include "Foo.h"
Foo& getBar() { static Foo bar; return bar; }

通常,静态成员不是指针而是对象。这样,您就可以自动销毁。@LokiAstari在大多数情况下,您不希望销毁,因为这会导致析构函数的顺序问题。@JamesKanze:我完全不同意。
通常
您确实希望销毁。(不希望销毁是例外)。这就是为什么这个习语通常使用对象。破坏顺序被定义为构造顺序的倒数,因此永远不会有问题。请参阅:真正的解决方案是更改代码。或者永远不要使用全局状态。Foo有任何方法吗?或者在使用时只是作为参数传递。在后一种情况下,您可以一个新类型,当用作Foo类型时会进行转换。不幸的是(对于这种情况,但通常幸运的是)你不能重写
来做一些有用的事情。不幸的是,这仍然失败,因为引用可能在使用之前没有初始化,就像变量可能没有初始化一样。是的,我不确定我在想什么。可能真的很难看,并定义了一个解析为bar()的宏条.但我不敢相信我提出了这个建议,非常讨厌的黑客行为。就我个人而言,我会进行搜索并替换。#define Foo Foo()正如mcnicholls所建议的那样,它似乎工作得非常好。原则上,我通常也会对使用宏感到畏缩,但我似乎找不到这样做失败的案例。不幸的是,这样做可能会失败:文件a.cpp:
aa;
它与Foo无关,而是使用bar.cpp中定义的全局对象栏。
bar&bar(){static bar b;return b;}
在Bar的构造函数中,它尝试访问
Foo-Bar;
此时可能尚未构造,因为这组调用是在编译单元A.cpp中启动的,可能尚未导入Foo.hh(它只需要导入Bar.h)。为了确保这一点有效,您必须在任何方法中使用Foo的所有头文件中包含Foo.hh(但这不是必需的,因为您可能会将它包含在bar.cpp中)。在使用匿名命名空间强制初始化本地版本的bar(这是一个引用)的情况下,存在此技术的更简单版本。但它失败的原因与上述完全相同。请参阅@LokiAstari it can fail,是的。正如在静态对象的构造函数中使用
std::cout
可能会失败一样。但这是您所能做的最好的方法
// Notice we don't need to include Foo.h we are not using it directly.
#include "Bar.h"

FOOOOOO::FOOOOOO()
{
      barbar().doBArStuff();     
}

// Here is where the problem occures.
FOOOOOO  FCUK_UP;

// If this object is constructed first.
// It will call barbar() in its constructor.
// Which will initialize an object of Foo in its constructor.
// But at this point we have not forced any initialization of `Foo bar`
extern Foo  bar;
#include "Foo.h"
Foo& bar;
Foo& getBar();

// Not like I like this but it would be better to force users to change
// how they use bar() rather than use this hack.
#define   bar    getBar()
#include "Foo.h"
Foo& getBar() { static Foo bar; return bar; }