Nifty / Schwarz计数器,符合标准?

时间:2011-04-11 14:14:12

标签: c++ static-order-fiasco

我今天早上和一位同事就静态变量初始化顺序进行了讨论。他提到了Nifty/Schwarz counter而我(有点)感到困惑。我理解它是如何工作的,但我不确定这在技术上是否符合标准。

假设以下3个文件(前两个是More C++ Idioms中的copy-pasta'd):


//Stream.hpp
class StreamInitializer;

class Stream {
   friend class StreamInitializer;
 public:
   Stream () {
   // Constructor must be called before use.
   }
};
static class StreamInitializer {
  public:
    StreamInitializer ();
    ~StreamInitializer ();
} initializer; //Note object here in the header.

//Stream.cpp
static int nifty_counter = 0; 
// The counter is initialized at load-time i.e.,
// before any of the static objects are initialized.
StreamInitializer::StreamInitializer ()
{
  if (0 == nifty_counter++)
  {
    // Initialize Stream object's static members.
  }
}
StreamInitializer::~StreamInitializer ()
{
  if (0 == --nifty_counter)
  {
    // Clean-up.
  }
}

// Program.cpp
#include "Stream.hpp" // initializer increments "nifty_counter" from 0 to 1.

// Rest of code...
int main ( int, char ** ) { ... }

......这就是问题所在!有两个静态变量:

  1. Stream.cpp中的“nifty_counter”;和
  2. “初始化程序”在Program.cpp
  3. 由于这两个变量恰好位于两个不同的编译单元中,因此没有(AFAIK)官方保证nifty_counterinitializer的构造函数之前被初始化为0被称为。

    我可以想到两个快速的解决方案,作为两个“工作”的原因:

    1. 现代编译器非常聪明,可以解决两个变量之间的依赖关系,并将代码以适当的顺序放在可执行文件中(极不可能);
    2. nifty_counter实际上是在“加载时”初始化的,就像文章所说的那样,它的值已经放在可执行文件的“数据段”中,所以它总是在“运行任何代码之前”初始化(极有可能)。
    3. 在我看来,这些都依赖于一些非官方但可能的实施。这个标准是否合规,或者这只是“非常可行”,我们不应该担心它?

2 个答案:

答案 0 :(得分:21)

我相信它确保有效。根据标准($ 3.6.2 / 1):“在进行任何其他初始化之前,具有静态存储持续时间(3.7.1)的对象应进行零初始化(8.5)。”

由于nifty_counter具有静态存储持续时间,因此无论跨翻译单元的分布如何,都会在创建initializer之前对其进行初始化。

编辑:在重新阅读相关部分并考虑@Tadeusz Kopec评论的输入后,我不太确定它是否已经定义好,因为它现在正确,但它 非常简单到确保它定义明确:从nifty_counter的定义中删除初始化,如下所示:

static int nifty_counter;

由于它具有静态存储持续时间,即使没有指定初始化程序也会进行零初始化 - 并且删除初始化程序会消除对零初始化后发生的任何其他初始化的任何疑问。

答案 1 :(得分:3)

我认为这个例子中缺少的是如何避免Stream的构造,这通常是不可移植的。除了漂亮的计数器,初始化者的角色是构建类似的东西:

extern Stream in;

如果一个编译单元具有与该对象关联的内存,则在使用就地新运算符之前是否存在某些特殊构造函数,或者在我看到的情况下,以另一种方式分配内存以避免任何冲突。在我看来,在这个流上有一个无操作的构造函数,然后是初始化者是先调用还是无操作构造函数的定义。

分配字节区域通常是不可移植的,例如对于gnu iostream,cin的空间定义为:

typedef char fake_istream[sizeof(istream)] __attribute__ ((aligned(__alignof__(istream))))
...
fake_istream cin;

llvm使用:

_ALIGNAS_TYPE (__stdinbuf<char> ) static char __cin [sizeof(__stdinbuf <char>)];

两者都对对象所需的空间做出了一定的假设。施瓦茨计数器的初始化位置是新的:

new (&cin) istream(&buf)

实际上这看起来不那么便携。

我注意到像gnu,microsoft和AIX这样的编译器确实有编译扩展来影响静态初始化顺序:

  • 对于Gnu,这是:使用init-priority标记启用-f并使用__attribute__ ((init_priority (n)))
  • 在使用Microsoft软件编译器的Windows上,有一个#pragma(http://support.microsoft.com/kb/104248