C++静态初始化顺序

81
在C++中使用静态变量时,通常会想要通过将一个变量传递给其构造函数来初始化另一个变量。换句话说,我想创建依赖于彼此的静态实例。
在单个.cpp或.h文件中,这不是问题:实例将按照声明的顺序创建。然而,当您想要使用另一个编译单元中的实例初始化静态实例时,似乎无法指定顺序。结果就是,取决于天气,可能会发生依赖于另一种实例的实例被构造,而仅在此之后才构造另一个实例。结果是第一个实例被错误地初始化。
有人知道如何确保静态对象按正确的顺序创建吗?我已经搜索了很长时间,尝试了所有方法(包括Schwarz计数器解决方案),但我开始怀疑是否真的有一种方法可以解决这个问题。
一个可能性是使用静态函数成员的技巧:
Type& globalObject()
{
    static Type theOneAndOnlyInstance;
    return theOneAndOnlyInstance;
}

确实,这样做是可以的。遗憾的是,您必须编写globalObject().MemberFunction()而不是globalObject.MemberFunction(),导致客户端代码有点混乱和不雅。

更新: 感谢您的回应。遗憾的是,看起来我确实已经回答了自己的问题。我想我必须学会接受它...


实例将按照它们被定义的顺序创建。 - bartolo-otrit
6个回答

76
您已经回答了您自己的问题。静态初始化顺序是未定义的,并且最优雅的方法(同时仍然进行静态初始化,即不完全重构它)是将初始化包装在一个函数中。
请阅读C++常见问题解答中有关静态初始化顺序的条目,从https://isocpp.org/wiki/faq/ctors#static-init-order开始阅读。

1
不是在主函数进入时返回,而是在第一次调用该方法时返回。请参见http://blogs.msdn.com/b/oldnewthing/archive/2004/03/08/85901.aspx。 - José
3
@enobayram: 将您带回了2.1年前的时间 :), 是的,这是一个问题。将初始化操作放入函数中的整个目的在于,初始化现在不是在main函数之前发生,而是在第一次调用函数时发生,这可能会在程序执行期间的任何时刻...和任何线程中发生。 - Lightness Races in Orbit
2
@Lightness,Jose,Charles:我真的认为你们的评论应该被纳入到被接受的答案中。随着多线程在今天变得普遍,这可能会导致令人不快的意外,并且在静态初始化惨案的顶部搜索结果中没有提到它。即parashift根本没有提到它。 - a.peganz
6
C++11 中不存在线程安全问题。请参阅 https://dev59.com/gGsz5IYBdhLWcg3wHUS0。 - Luke Worth
1
@laalto,阅读C++ FAQ让我们回到了“你是否阅读过手册页”的时代。 - SimplyKnownAsG
显示剩余3条评论

7
也许你应该重新考虑是否需要这么多全局静态变量。虽然它们有时很有用,但通常将它们重构为更小的本地作用域会更简单,特别是如果您发现一些静态变量依赖于其他变量。
但是你是正确的,无法保证特定的初始化顺序,所以如果你想要实现它,像你提到的那样,在一个函数中进行初始化可能是最简单的方法。

4
你说得对,过多使用全局静态变量是不明智的,但在某些情况下,它可以避免过度传递相同的对象。比如日志记录器对象、持久变量的容器、所有IPC连接的集合等。 - Dimitri C.
2
“避免过度传递对象”只是一种说法,“我的程序组件之间的依赖关系非常复杂,跟踪它们是一项繁琐的工作。因此最好停止跟踪它们”。除非通常情况下并不是更好的选择——如果依赖关系无法简化,那么能够通过跟踪对象传递的位置来追踪它们就变得*最为有用。 - Steve Jessop

7

大多数编译器(链接器)实际上都支持一种(不可移植的)指定顺序的方式。例如,使用Visual Studio,您可以使用init_seg pragma将初始化排列成几个不同的组。据我所知,在每个组内部无法保证顺序。由于这是不可移植的,您可能希望考虑是否可以修复设计而不需要它,但该选项确实存在。


5
确实,这是可行的。遗憾的是,你必须编写globalObject().MemberFunction(),而不是globalObject.MemberFunction(),这导致客户端代码有些混乱和不雅观。
但最重要的是它能工作,并且它是失效证明的,也就是说,很难绕过正确用法。
程序的正确性应该是你的首要任务。此外,在我看来,上面的括号纯粹是样式上的——也就是完全不重要的。
根据你的平台,要小心过多的动态初始化。有一定量的清理可以针对动态初始化程序(见here)。你可以使用一个包含多个成员不同全局对象的全局对象容器来解决这个问题。因此,你有:
Globals & getGlobals ()
{
  static Globals cache;
  return cache;
}

您的程序中只需要一次调用~Globals()来清理所有全局对象。为了访问全局变量,您仍然需要像这样做:
getGlobals().configuration.memberFunction ();

如果你真的想要,你可以将这个内容包装在一个宏中,以节省一点打字时间:

#define GLOBAL(X) getGlobals().#X
GLOBAL(object).memberFunction ();

虽然这只是您初始解决方案的语法糖。


3

尽管这个主题已经很老了,但我想提出我找到的解决方案。正如许多人在我之前指出的那样,C++没有提供任何静态初始化顺序的机制。我建议将每个静态成员封装在类的静态方法中,该静态方法会初始化成员并以面向对象的方式提供访问。

让我举个例子,假设我们想要定义一个名为“Math”的类,其中包含“PI”等其他成员:

class Math {
public:
   static const float Pi() {
       static const float s_PI = 3.14f;
       return s_PI;
   }
}

在GCC中,s_PI将在第一次调用Pi()方法时初始化。需要注意的是,具有静态存储的局部对象具有与实现相关的生命周期,请查看2中的6.7.4以了解更多细节。

Static关键字, C++标准


2
除了你将函数作为类的成员之外,这与 OP 的方法有何不同? - einpoklum
这并不是一个启示性的例子,因为像那样的对象可以且应该是constexpr的。 - underscore_d

1

将静态内容包装在一个方法中可以解决顺序问题,但正如其他人指出的那样,它并不是线程安全的,但如果有需要,你可以这样做来使其线程安全。

// File scope static pointer is thread safe and is initialized first.
static Type * theOneAndOnlyInstance = 0;

Type& globalObject()
{
    if(theOneAndOnlyInstance == 0)
    {
         // Put mutex lock here for thread safety
         theOneAndOnlyInstance = new Type();
    }

    return *theOneAndOnlyInstance;
}

2
最佳答案正是建议这样做的,而且它在将近5年前就已经回答了。也许我们应该将其作为示例移动到最佳答案中。 - AndyG
2
这在C++11中不是问题。请参见https://dev59.com/gGsz5IYBdhLWcg3wHUS0 - Luke Worth

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接