通过Schwartz Counter进行C++静态初始化

Dav*_*ave 13 c++

施瓦茨计数器是为了保证在使用前一个全局对象初始化.

请考虑使用下面显示的Schwartz计数器.

文件Foo.h:

class Foo
{
   Foo::Foo();
};
Run Code Online (Sandbox Code Playgroud)

文件Foo.cpp:

#include "Foo.h"

// Assume including Mystream.h provides access to myStream and that
// it causes creation of a file-static object that initializes
// myStream (aka a Schwartz counter).
#include "MyStream.h"

Foo::Foo()
{
   myStream << "Hello world\n";
}
Run Code Online (Sandbox Code Playgroud)

如果在main()启动后运行Foo :: Foo(),则保证myStream的使用是安全的(即myStream将在使用前初始化),因为注释中提到了文件静态初始化对象.

但是,假设在main()启动之前创建了Foo实例,如果它是全局的则会发生.这显示在这里:

File Global.cpp:

#include "Foo.h"

Foo foo;
Run Code Online (Sandbox Code Playgroud)

请注意,Global.cpp不会像Foo.cpp那样获得文件静态初始化对象.在这种情况下,Schwartz计数器如何确保在foo之前初始化MyStream初始化程序(以及MyStream对象本身)?或者Schwartz计数器在这种情况下会失败吗?

Die*_*ühl 17

采用"施瓦茨计数器"(杰里·施瓦茨后,所谓的谁设计的Iostreams库的基础,因为它现在是在标准;请注意,他不能被指责为许多奇怪的选择,因为这些被标记到原始设计)可以导致在构造对象之前访问它们.最明显的情况是在构造全局对象期间调用一个函数,该对象使用通过Schwartz计数器构建的自己的全局来调用另一个翻译单元(我使用std::coutSchwartz计数器保护全局,以保持示例简短) :

// file a.h
void a();

// file a.cpp
#include <iostream>
void a() { std::cout << "a()\n"; }

// file b.cpp
#include <a.h>
struct b { b() { a(); } } bobject;
Run Code Online (Sandbox Code Playgroud)

如果文件b.cpp中的全局对象是在文件中的全局对象之前构造的,a.cpp并且如果std::cout是通过a.cpp作为第一个实例的Schwartz计数器构造的,则此代码将失败.至少有两个原因导致Schwartz计数器不能正常工作:

  1. 为此使用全局对象时,此对象最终会被构造两次.虽然这在正确的情况下在实践中起作用,但我认为这很难看.解决此问题的方法是使用char适当大小的缓冲区来实际定义对象(这些缓冲区通常会被伪装成名称作为正确类型的对象).然而,在这两种情况下,事情都很混乱.
  2. 当Schwartz计数器保护的全局对象在许多转换单元中使用时(如果是这样std::cout),这可能会导致显着的启动延迟:编写良好的代码通常不使用任何全局初始化,但Schwartz计数器需要为每个需要加载的目标文件运行一段代码.

我个人已经得出结论,这种技术是一个很好的想法,但它在实践中不起作用.我使用三种方法代替:

  1. 不要使用全局对象.这使得整个讨论过时并且在并发代码中工作得最好.在绝对需要全局资源的情况下,通过引用返回并使用初始化的函数静态对象std::call_once()是更好的替代方案.
  2. 在链接可执行文件(例如,last)时将全局对象放置在适当的位置会导致首先初始化它.我过去曾经尝试过这个,然后我发现我可以在所有我关心的系统上正确放置目标文件.这里的主要缺点是没有保证,并且在编译器版本之间切换时可能会发生变化.但是对于C++标准库,这是可以接受的(当我这样做时,我只关心全局流对象).
  3. 将全局对象放入专用共享库:加载共享库时,将执行其初始化代码.只有在初始化完成后,共享库中的对象才可用.我发现这种方法可靠但需要额外的库.