C ++静态初始化顺序


68

当我在C ++中使用静态变量时,我常常最终想要初始化一个变量,并将另一个传递给其构造函数。换句话说,我想创建相互依赖的静态实例。

在单个.cpp或.h文件中,这不是问题:实例将按照声明的顺序创建。但是,当您要使用另一个编译单元中的实例初始化静态实例时,似乎无法指定顺序。结果是,取决于天气,可能会构造依赖另一个实例的实例,然后才构造另一个实例。结果是第一个实例未正确初始化。

有谁知道如何确保以正确的顺序创建静态对象?我已经寻找了很长时间的解决方案,尝试了所有解决方案(包括Schwarz Counter解决方案),但是我开始怀疑是否有真正可行的解决方案。

一种可能是使用静态函数成员的技巧:

Type& globalObject()
{
    static Type theOneAndOnlyInstance;
    return theOneAndOnlyInstance;
}

确实,这确实有效。遗憾的是,您必须编写globalObject()。MemberFunction()而不是globalObject.MemberFunction(),从而导致客户端代码有些混乱和不雅观。

更新:感谢您的反应。遗憾的是,确实好像我已经回答了我自己的问题。我想我必须学会忍受...


实例将按照 定义
bartolo-otrit

Answers:


65

您已经回答了自己的问题。静态初始化顺序是不确定的,而围绕它的最优雅的方法(尽管仍在进行静态初始化,即不完全重构它)是将初始化包装在一个函数中。

阅读从https://isocpp.org/wiki/faq/ctors#static-init-order开始的C ++常见问题解答项目。


@CharlesSalvia带您回到2.5年前:),这不是线程安全的,这真的有问题吗?我们正在谈论静态初始化,因此,这一切都在发生之前main()。我们以前真的应该担心线程安全问题main()吗?
enobayram'4

1
不是在main进入之前,而是在首次调用该方法时。看到blogs.msdn.com/b/oldnewthing/archive/2004/03/08/85901.aspx
何塞

3
@enobayram:带您回到2.1年:),是的,这是一个问题。将初始化移入函数的全部要点是,初始化现在不是在before之前发生的main,而是在首次调用函数时发生的,这可能在程序执行期间的任何时候……以及在任何线程中进行。
Lightness Races in Orbit

2
@Lightness,何塞,查尔斯:我真的认为您的评论应纳入接受的答案。在当今多线程很普遍的情况下,这可能会导致令人讨厌的意外,并且在静态初始化惨败的任何顶级搜索结果中都没有提及。也就是说,parashift根本没有提到它。
a.peganz 2014年

5
在C ++ 11中,线程安全性不是问题。见stackoverflow.com/questions/8102125/...
卢克·沃斯

7

也许您应该重新考虑是否需要这么多的全局静态变量。尽管有时它们很有用,但将它们重构到较小的局部范围通常要简单得多,尤其是当您发现某些静态变量依赖于其他变量时。

但是您是对的,没有办法确保初始化的特定顺序,因此,如果您心甘情愿,则将初始化保持在一个函数中(如您所述)可能是最简单的方法。


4
没错,使用太多的全局静态变量是不明智的,但是在某些情况下,它避免了过多传递同一对象。想想记录器对象,持久变量的容器,所有IPC连接的集合等...
Dimitri C.

2
“避免过度传递对象”只是一种说法,“程序组件之间的依赖关系如此之广,以至于无法跟踪它们。因此最好停止跟踪它们”。除非通常不是更好-如果无法简化依赖关系,那么这是通过跟踪对象的传递位置来对其进行跟踪的有用的时间。
Steve Jessop

5

确实,这确实有效。遗憾的是,您必须编写globalObject()。MemberFunction()而不是globalObject.MemberFunction(),从而导致客户端代码有些混乱和不雅观。

但是最重​​要的是,它可以正常工作,并且可以防止故障。绕过正确用法并不容易。

程序正确性应该是您的首要任务。另外,恕我直言,上面的()纯粹是风格上的-即 完全不重要。

取决于您的平台,请注意过多的动态初始化。动态初始化程序可以进行的清理相对较少(请参见此处)。您可以使用包含成员不同全局对象的全局对象容器来解决此问题。因此,您有:

Globals & getGlobals ()
{
  static Globals cache;
  return cache;
}

只有一个调用〜Globals()才能清理程序中的所有全局对象。为了访问全局,您仍然需要:

getGlobals().configuration.memberFunction ();

如果您真的想要,可以将其包装在宏中,以节省使用宏的一点点输入:

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

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


5

实际上,大多数编译器(链接器)都支持(不可移植的)指定顺序的方式。例如,在Visual Studio中,您可以使用init_seg编译指示将初始化分为几个不同的组。AFAIK无法保证每个组内的订单。由于这是不可移植的,因此您可能要考虑是否可以将设计固定为不需要它,但是可以选择。


3

尽管该线程的年代久远,但我想提出我发现的解决方案。正如我之前指出的那样,C ++不提供任何用于静态初始化排序的机制。我的建议是将每个静态成员封装在类的静态方法内,该类依次初始化该成员并以面向对象的方式提供访问。让我举一个例子,假设我们要定义一个名为“ Math”的类,该类除其他成员外还包含“ PI”:

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

第一次调用Pi()方法时(在GCC中),将初始化s_PI。请注意:具有静态存储的本地对象具有与实现相关的生命周期,有关更多详细信息,请参阅6.7.4 in 2

静态关键字C ++标准


2
除了您使函数成为类的成员之外,OP的这种方法有何不同?
einpoklum 2015年

也不是一个有启发性的例子,因为这样的对象可以并且应该是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;
}

1
最佳答案恰恰表明了这一点,并且在将近5年前就得到了答案。也许我们应该将其移至最高答案作为示例。
AndyG

1
在C ++ 11中这不是问题。见stackoverflow.com/questions/8102125/...
卢克·沃斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.