4

在学习 isocpp 时,我遇到了一个FAQ。其中说:“什么是保证静态初始化和静态反初始化的技术?” 简短的回答只是暗示:

简短的回答:使用 Nifty Counter Idiom(但请确保您了解重要的权衡!)。

到目前为止,我无法理解Nifty Counter Idiom是什么意思以及它如何解决 Fiasco 问题。我已经知道Construct On First Use Idiom及其在使用静态对象或静态指针时的副作用(包装在返回引用的全局函数中)

这是一个实际上容易出现 Fiasco 问题的简单示例:

//x.cpp
include "x.hpp" // struct X { X(int); void f(); };

X x{ 10 };
struct Y { Y(); };
Y::Y(){ x.f(); };

//y.cpp
include <iostream>
include "y.hpp" // struct Y { Y(); };

struct X { X(int); void f(); private: int _x; };
X::X(int i) { _x = i; }; 
void X::f() { std::cout << _x << std::endl; };
Y y;

这里,它提到了 Nifty Counter Idiom 的主要用途

意图:确保非本地静态对象在第一次使用前被初始化,并且仅在对象最后一次使用后被销毁。

现在我需要的是,Nifty Counter Idiom具体如何解决上述代码中的静态订单初始化和反初始化,而不管其他解决方法为constinit. 鉴于上述程序是这样编译的:

~$ g++ x.cpp y.cpp && ./a.out>> 10

~$ g++ y.cpp x.cpp && ./a.out>> 0

4

2 回答 2

2

嗯,这一个漂亮的计数器机制。它不会“修复”订购惨败;相反,它正在做的是使用一个整数计数器来制作它,以便编译器执行其静态初始化和静态破坏的(仍然未定义的)顺序无关紧要

它是如何做到的?简单的; 无论哪个StreamInitializer对象的静态初始化首先发生,StreamInitializer()构造函数首先要做的就是 increment nifty_counter。由于nifty_counter默认初始化为零,这意味着if (nifty_counter++ == 0)构造函数中的测试将仅返回一次 true,即第一次执行时,无论从哪个 .cpp 文件触发初始化。这将导致Stream对象的放置新初始化仅发生一次,这是第一次初始化的副作用StreamInitializer

同样,每个~StreamInitializer()析构函数都会递减,并且由于析构函数调用(在程序执行结束时)与构造函数调用(在程序执行开始时)nifty_count的数量完全相同,因此我们可以保证它将是最后一次调用该减量回零。无论编译器选择破坏发生的顺序如何,这都是正确的,这意味着对象的破坏只会在最后一次调用期间发生,这就是您想要的。~StreamInitializer()StreamInitializer()~StreamInitializer()nifty_counterStream~StreamInitializer()

结果是只要您的 .cpp 文件StreamInitializer首先声明一个对象,它就可以安全地访问该Stream对象,因为有效StreamInitializer对象的存在(特别是在适当的时间执行其构造函数和析构函数)保证了Streamobject 将在该 .cpp 文件中的任何后续静态对象构造函数/析构函数代码中有效。

于 2021-12-12T06:11:54.210 回答
2

我认为您提供的链接很好地解释了它。但这是我的尝试。

首先是什么问题

// stream.hpp

struct Stream {
  Stream ();
  ~Stream ();
  void operator<<(int);
};

extern Stream stream; // global stream object
// stream.cpp

#include "stream.hpp"

Stream::Stream () { /* initialize things */ }
Stream::~Stream () { /* clean up */ }
void Stream::operator<<(int a) { /* write to stream */ }

Stream stream{};
// app.cpp

#include "stream.hpp"

struct X
{
    X()
    {
         stream << 24; // use stream object
    }
    ~X()
    {
         stream << 1024; // use stream object
    }

};

X x{}; // in this static initialization the static stream object is used

C++ 标准不保证跨 TU 的静态对象的任何初始化顺序。stream并且x在不同的 TU 中,所以stream可以先初始化,这很好。或者x可能在stream这之前被初始化是不好的,因为x使用stream的初始化尚未初始化。

如果stream在之前被破坏,析构函数也会出现同样的问题x

漂亮的计数器解决方案

它利用了在 TU 中对象按顺序初始化和销毁​​的事实。但是我们不能stream每个 TU 都有一个对象,所以我们使用了一个技巧,而是StreamInitializer为每个 TU 使用一个对象。这streamInitializer将在之前构造x并在之后销毁x,我们确保只有第一个streamInitializer被构造的创建流对象,只有最后一个streamInitializer被破坏的才会破坏流对象:

// stream.hpp

struct Stream {
  Stream ();
  ~Stream ();
  void operator<<(int);
};
extern Stream& stream; // global stream object

struct StreamInitializer {
  StreamInitializer ();
  ~StreamInitializer ();
};

static StreamInitializer streamInitializer{}; // static initializer for every TU
// stream.cpp

#include "stream.hpp"

static int nifty_counter{}; // zero initialized at load time
static typename std::aligned_storage<sizeof (Stream), alignof (Stream)>::type
  stream_buf; // memory for the stream object
Stream& stream = reinterpret_cast<Stream&> (stream_buf);

Stream::Stream () { // initialize things }
Stream::~Stream () { // clean-up } 

StreamInitializer::StreamInitializer ()
{
  if (nifty_counter++ == 0) new (&stream) Stream (); // placement new
}
StreamInitializer::~StreamInitializer ()
{
  if (--nifty_counter == 0) (&stream)->~Stream ();
}

我们的app.cpp是一样的

// app.cpp

#include "stream.hpp"

struct X
{
    X()
    {
        stream << 24; // use stream object
    }
    ~X()
    {
         stream << 1024; // use stream object
    }

};

X x{}; // in this static initialization the static stream object is used

现在stream不是一个值,它是一个参考。所以在这一行中Stream& stream = reinterpret_cast<Stream&> (stream_buf);没有Stream创建任何对象。stream_buf只是引用绑定到内存中仍然不存在的对象。该对象将在稍后构建。

每个编译单元都有一个streamInitializer对象。这些对象以任何顺序初始化,但这并不重要,因为只有其中一个nifty_counter将是0并且在那个上并且只有在那个上new (&stream) Stream ();才会执行该行。这一行实际上stream在内存位置创建了对象stream_buf

现在为什么X x{};在这种情况下线路正常?因为这个TU的streamInitializer对象是在之前初始化的x,这就保证了stream在之前的初始化xstream.hpp这对于在使用stream对象之前正确包含的每个 TU 都是如此。

相同的逻辑适用于析构函数,但顺序相反。

回顾

不同 TU 中的静态对象可以按任意顺序初始化。如果来自一个 TU 的一个静态对象在它的构造函数或析构函数中使用来自另一个 TU 的另一个静态对象,则会出现问题。

在 TU 中,对象按顺序构造和销毁。

在漂亮的 connter 成语中:

  • 一个 TU 持有流对象的原始内存
  • stream是对该内存位置的对象的引用
  • streamInitializer在每个使用该对象的 TU 中都会创建一个对象stream
  • 只有 1streamInitializer会创建流对象,第一个被初始化
  • 在每次使用之前在一个 TU 中x,至少该streamInitializerTU 已经初始化。这意味着至少streamInitializer构造了一个,这意味着在stream构造函数之前构造了x.

包括为简洁而省略的警卫

于 2021-12-12T06:38:45.803 回答