漂亮/施瓦茨计数器,标准兼容

Nifty/Schwarz counter, standard compliant?

本文关键字:标准 计数器 施瓦茨 漂亮      更新时间:2023-10-16

今天早上我和同事讨论了静态变量初始化顺序的问题。他提到了俏丽/施瓦茨计数器,我(有点)困惑。我知道它是如何工作的,但我不确定从技术上讲,这是否符合标准。

假设以下3个文件(前两个是从More c++习语中复制过来的):


//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. "nifty_counter" in Stream.cpp;和
  2. Program.cpp中的"initializer"

由于这两个变量恰好在两个不同的编译单元中,因此没有(AFAIK) 官方保证在调用initializer的构造函数之前将nifty_counter初始化为0。

我可以想到两个快速的解决方案来解释为什么这个"工作":

  1. 现代编译器足够聪明,可以解决两个变量之间的依赖关系,并将代码按适当的顺序放在可执行文件中(可能性很小);
  2. nifty_counter实际上是在"加载时"初始化的,就像文章说的那样,它的值已经放在可执行文件的"数据段"中,所以它总是在"任何代码运行之前"初始化(极有可能)。

在我看来,这两个都依赖于一些非官方的,但可能的实现。这个标准是兼容的还是仅仅是"很可能工作",我们不应该担心它?

我相信它一定会起作用。根据标准($3.6.2/1):"具有静态存储时间(3.7.1)的对象应在任何其他初始化发生之前进行零初始化(8.5)。"

由于nifty_counter具有静态存储持续时间,因此它在initializer创建之前被初始化,而不考虑跨翻译单元的分布。

编辑:在重读有问题的部分后,并考虑到@Tadeusz Kopec的评论输入,我不太确定它是否定义良好,因为它现在,但它相当琐碎,以确保它是定义良好的:从nifty_counter的定义中删除初始化,所以它看起来像:
static int nifty_counter;

由于它具有静态存储持续时间,因此它将被零初始化,即使没有指定初始化式——并且删除初始化式可以消除对在零初始化之后发生的任何其他初始化的任何疑问。

我认为这个例子缺少的是如何避免Stream的构造,这通常是不可移植的。除了漂亮的计数器之外,初始化器的作用是构造如下内容:

extern Stream in;

当一个编译单元拥有与该对象相关联的内存时,在使用就地new操作符之前是否存在一些特殊的构造函数,或者在我见过的以另一种方式分配内存以避免任何冲突的情况下。在我看来,如果在这个流上有一个无操作构造函数,那么是先调用初始化函数还是不定义无操作构造函数的顺序。

要分配一个字节区域通常是不可移植的,例如对于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>)];

都对物体所需的空间做了一定的假设。其中Schwarz计数器初始化的位置为new:

new (&cin) istream(&buf)

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

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

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