繁体   English   中英

C++ Nifty Counter 成语如何保证 static 初始化和 static 去初始化

[英]How does C++ Nifty Counter idiom guarantee both static initialization and static deinitialization

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

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

到目前为止,我无法理解Nifty Counter Idiom是什么意思以及它如何解决 Fiasco 问题。 I am already aware of Construct On First Use Idiom , and its side effects while using a static object or a static pointer (wrapped in a global function returning reference)

这是一个实际上容易出现 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 的主要用途

意图:确保非本地 static object 在首次使用前被初始化,并且仅在最后一次使用 object 后被销毁。

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

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

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

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

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

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

The upshot is that as long as your.cpp file declares a StreamInitializer object first, it can safely access the Stream object, because the presence of a valid StreamInitializer object (and in particular, the execution of its constructor and destructor at the appropriate times)保证Stream object 在 that.cpp 文件中的任何后续静态对象构造函数/析构函数代码中都是有效的。

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

首先是什么问题

// 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 的 static 对象的任何初始化顺序。 streamx在不同的 TU 中,所以可以先初始化stream ,这样就可以了。 或者x可以在stream之前初始化,这很糟糕,因为x的初始化使用了尚未初始化的stream

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

漂亮的计数器解决方案

它利用了在 TU 中对象按顺序初始化和销毁的事实。 但是我们不能为每个 TU 使用stream object,所以我们使用了一个技巧,我们改为为每个 TU 使用StreamInitializer object。 This streamInitializer will be constructed before x and destructed after x and we make sure that only the first streamInitializer that gets constructed creates the stream object and only the last streamInitializer to be destructed destroys the stream object:

// 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 object。 只是引用绑定到仍然不存在的 object,它位于stream_buf memory 中。 这个 object 稍后会构建。

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

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

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

回顾

不同TU中的Static对象可以按任意顺序初始化。 如果一个 TU 的一个 static object 在其构造函数或析构函数中使用另一个 static26696CFDE63131ZCBBEB6

在 TU object 中按顺序构造和销毁。

在漂亮的 connter 成语中:

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

包括为简洁而省略的警卫

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM