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; }