C++ 静态初始化顺序:解决旧问题的方案






4.03/5 (8投票s)
提出一种简单的控制静态对象初始化顺序的方法。
引言
过去在 CodeProject 上关于这个问题有过很长的讨论,尽管目前似乎没有了,而且有些人可能认为这是一个过时的议题。出于两个很好的原因,我不这么认为。首先,总有新的开发者,尤其是在这样的论坛上。其次,我从未对之前看到过的任何解决方案完全满意。总的来说,我认为这篇文章属于中级水平,可能会让一些初学者感到困惑(我倾向于使用技术术语),但希望不会让你们这些 C++ 大师感到不屑。对于那些从未遇到过这个问题的人来说,快速回顾一下这个问题是必要的。
问题
当你在 C++ 项目中声明一个以上类型的 `static` 实例时,无法保证它们的构造顺序。这不是编译器的错误,而是 C++ 规范的一部分。
例如
//A.h
class A
{
public:
A();
~A();
...
};
//B.h
class B
{
public:
B();
~B();
...
};
//A.cpp
A s_A;
//B.cpp
B s_B;
先调用哪个构造函数,`A()` 还是 `B()`?
答案是,这完全取决于编译器碰巧以什么顺序组合代码,下次你更改了一些“不相关”的东西并再次构建时,它们可能会以相反的顺序调用。
为什么这很重要?
当 `A()` 依赖于 `s_B` 已经被构造(例如通过调用)时,这变得非常重要。
s_B.DebugPrint( _T("s_A is being constructed") );
另一个相关的问题是,`A()` 和 `B()` 都将在底层运行时库仍在引导自身时被调用,如果它们尝试做一些“过于聪明”的事情,例如检查传递给程序的命令行参数,就会发生糟糕的事情,很可能会在调试器甚至能够连接以让你找出问题所在之前就导致程序崩溃。这很可能会带来糟糕的一天和不愉快的回家,谁知道这会造成多大的麻烦。
这种 `static` 初始化顺序依赖确实会发生,并且在实际部署的软件中,有些软件依赖于构建顺序的巧合才能启动。这是一个非常可怕的想法!
一个解决方案应该是什么样的?
有些人会说解决方案是不使用 `static` 实例,但这既不实用也不是真正的解决方案。`Static` 实例化在语言中存在是有充分理由的,并且在某些有限的情况下确实应该使用。我们真正希望能够做的是利用 `static` 实例的所有特性,同时增加保证在运行时库引导阶段不会发生对象构造,并且 `static` 对象将在首次使用时被构造。通过这种方式,我们通过使用顺序控制初始化顺序,并确保在我们的 `static` 实例的构造函数被调用之前,运行时已完全运行并且 `main`/`Winmain` 已经被调用。
显然,没有零成本的简单解决方案能够为我们带来这些特性,任何解决方案都应该是轻量级的、低开销的、易于使用的、易于理解的等等,就像所有此类解决方案都应该的那样。我多年来见过不少提议的解决方案,它们都存在一个或多个性能、类型开销或对不同类型的适用性方面的局限性。
我迄今为止最好的尝试叫做静态模板对象块 (sTOB)。我把它分享给你,以防其他人仍然被这个相当老但棘手的问题困扰。
静态模板对象块
限制
- 它仅适用于具有默认构造函数的类对象。
我不认为这是一个很大的问题。非类对象不太可能引起初始化顺序问题,因为它们没有需要调用尚未初始化的东西的构造函数。没有默认构造函数的类反正也不会作为静态对象使用。 - 每次调用 `sTOB` 都会有少许开销,但在千兆赫兹处理器的时代,为了获得的好处,我可以接受。
- 每个 `sTOB` 都有一个微小的内存分配大小开销,约为六个字节。如果你有大量的 `static` 实例数组,并且这是一个问题,那么将它们放在一个 `static` 的“包装器”对象内,你将只需要支付一次开销。
- 你不能将 `sTOB` 实例声明为其模板参数类的成员,因为在编译器尝试这样做时,它无法计算 `sizeof T` 来使 `sTOB` 定义生效。这是一个相当令人恼火的限制,希望能够找到一个解决方案。
为了清楚起见:以下代码将无法编译 :-(class CSomething { public: Declare_pseudo_static( CSomething ) s_Something; };
代码
概念是静态地分配对象的内存,但直到首次调用之前才在该内存中构造它。对象保持“static
”,即它具有 `static` 实例的所有特性,除了 sTOB 在大多数情况下被视为指针而不是引用类型。可以将其视为一个简单的智能指针,你无需初始化它。
下面的代码是逐字逐句的头文件,注释已移至非代码文本。你可以重新组合它,或者直接使用附加演示代码中的实际代码。
//sTOB.h
//Rev 1.0
#include< new >
在此包含标准库 `
template< class T >
struct sTOB
{
由于这是演示代码,我将把类中 `private` 的有趣部分放在前面
private:
一个指向内存数组起始位置的实际 `T` 指针。`union` 和 `dStuffing` 成员纯粹是为了保证后面内存数组的对齐,这是 Billy E. 建议的。
union
{
T* m_pThis;
double dStuffing;
};
一个足够大的内存数组,可以容纳一个 `T`
char m_InternalData[sizeof T];
用于记录首次调用是否已发生的开关
bool m_bInitialised;
`_Kick` 宏确保实际的 `T` 在静态分配的内存中被创建。这是调用开销的原因,但它实际上只在每种情况下插入少量汇编指令。
#define _Kick \
if( !m_bInitialised ) \
{ \
/*Set initialized to true first to*/ \
/*allow codependent constructions*/ \
m_bInitialised = true; \
/*T() is called indirectly here*/ \
m_pThis = new(&m_InternalData[0]) T; \
}
现在是类的 `public` 接口部分
public:
我们需要重载所有这些运算符。客户端代码可能会在第一次使用它们中的任何一个来访问 `sTOB`,因此每个都必须 `_Kick`。
`sTOB` 可以从一个实际的 `T` 赋值
T& operator = (T _t)
{
_Kick
*m_pThis = _t;
return *m_pThis;
}
将 `sTOB` 转换为一个简单的 `T*`
operator T*()
{
_Kick
return m_pThis;
}
一个函数可以接受一个 `T&` 并且一个 `sTOB
operator T&()
{
_Kick
return *m_pThis;
}
智能指针 `T*` 的地址应该是 `T**`,对吧。
T** operator &()
{
_Kick
return &m_pThis;
}
这允许客户端代码通过 `sTOB` 调用,例如 `mysTOB->GetCmdLine();`
T* operator ->()
{
_Kick
return m_pThis;
}
在 `static` 初始化时构造 `sTOB`
sTOB()
{
m_bInitialised = false;
这设置了 `m_pThis` 的正确实际值,但它在构造完成后才有效
m_pThis = reinterpret_cast<T*>(m_InternalData);
}
在 `static` 拆卸时析构 `sTOB`。请注意,到调用此函数时,包括一些运行时组件在内的所有内容可能已经消失。
~sTOB()
{
if(m_bInitialised)
{
在此调用 `T` 析构函数
m_pThis->T::~T();
}
}
};
这为我们提供了静态分配和首次调用构造的保证。为了透明地利用这一点,我们需要另外几个简单的宏。
用于类成员声明
#define Declare_pseudo_static( _I ) static sTOB< _I >
用于文件作用域声明
#define Implement_pseudo_static( _I ) sTOB< _I >
现在我们可以这样写
//A.h
class A
{
public:
A();
~A();
...
};
//A.cpp
Implement_pseudo_static( A ) s_A;
//B.h
class B
{
public:
B();
~B();
...
};
//B.cpp
Implement_pseudo_static( B ) s_B;
现在,无论 `A()` 是否依赖于 `s_B`,或者 `B()` 是否依赖于 `s_A`,甚至是两者都依赖,都无关紧要了。
A::A()
{
s_B->Bark();//Safe as s_B gets constructed if it isn't already
}
B::B()
{
s_A->Squawk();//Safe as S_A gets constructed if it isn't already
}
进一步改进
`sTOB(s)` 让你能够控制构造顺序,但在当前实现中无法控制析构顺序。可以添加一个 `Free` 函数,可以直接在 `sTOB` 上调用,例如 `s_A.Free()`,它将调用内部对象析构函数并重置 `m_bInitailised`。这将在运行时在为你析构之前,提供确定性析构顺序的选项。我还没有需要过这个,可能是因为 `static` 对象通常在设计上就 intended 拥有最大的作用域和生命周期。
演示代码
演示代码是一个 VC8 解决方案, intended 在 Visual Studio IDE 中以 Debug 模式运行。它演示了如何使用 `sTOB` 来控制 `static` 初始化顺序,与不受控制的标准 `static` 对象进行比较。它应该能够轻松地移植到 MSVC6,因为模板的使用非常简单,但我没有尝试过。请注意,这只是为了演示目的而编写的极简代码。
结论
这是我为 CodeProject 写的第一个文章,它确实让我非常仔细地审视了我即将发布的代码,并发现了其中的许多缺陷。我相信还有更多需要发现,但这个过程本身是令人鼓舞的:它让我的代码变得更好。如果有感兴趣的人,我可能会写另一篇文章介绍我称之为 Flyer Instancing 的技术。
我欢迎大家提出评论,特别是关于改进和扩展这个想法的建议,或者它有哪些好的应用。
致谢
我要感谢 Marc Clifton 为他提交 CodeProject 文章所做的出色工作,没有他我可能永远无法完成这篇文章,以及感谢 Jason Henderson 的 Article Helper 工具,没有它这篇文章就无法发布。
修订历史
- 2007年5月29日:已更新以考虑 `char` 数组潜在的对齐问题。请参阅 Billy E. 的文章评论。
- 2007年5月26日:原始文章