简体   繁体   中英

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

While learning from isocpp, I came across an FAQ . Which says: "What is a technique to guarantee both static initialization and static deinitialization?" And the short answer is just hinted out:

Short answer: use the Nifty Counter Idiom (but make sure you understand the non-trivial tradeoffs.).

Till now, I can't understand what doesNifty Counter Idiom mean and how it fixes the Fiasco problem. 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)

Here's a simple example actually prone to a Fiasco problem:

//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;

Fromhere , it's mentioned the main use of Nifty Counter Idiom

Intent: Ensure a non-local static object is initialized before its first use and destroyed only after last use of the object.

Now what I need now is, how does Nifty Counter Idiom specifically can solve both static order initialization and deinitialization in the above code, regardless of other workarounds as constinit . Given that the above program is compiled like this:

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

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

Hmm, that is a nifty counter mechanism. It doesn't "fix" the ordering-fiasco; rather, what it is doing is using an integer counter to make it so that the (still undefined) order in which the compiler does its static-initialization and static-destruction doesn't matter .

How does it do that? Simple; no matter which StreamInitializer object's static-initialization happens first, the first thing the StreamInitializer() constructor will do is increment nifty_counter . Since nifty_counter was default-initialized to zero, that means the if (nifty_counter++ == 0) test in the constructor will return true only once, the first time it is executed, regardless of which.cpp file the initialization is being triggered from . That will cause the placement-new initialization of the Stream object to happen exactly once, as a side-effect of the first StreamInitializer being initialized.

Similarly, each ~StreamInitializer() destructor will decrement nifty_count , and since there will be exactly the same number of ~StreamInitializer() destructor-calls (at the end of the program's execution) as there were StreamInitializer() constructor-calls (at the beginning of the program's execution), we are guaranteed that it will be the very last call to ~StreamInitializer() that decrements nifty_counter back to zero. That will be true regardless of the order that the compiler chooses for destruction to happen , which means that the destruction of the Stream object will occur only during the last ~StreamInitializer() call, which is what you want.

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) guarantees that the Stream object will be valid in any subsequent static-object-constructor/destructor code in that.cpp file.

I think the link you provided explains it pretty well. But here's my attempt at it.

First what is the problem

// 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

The C++ standard does not guarantee any order of initialization of static objects across TUs. stream and x are in different TUs, so stream could be initialized first, which is fine. Or x could be initialized before stream which is bad because the initialization of x uses stream which is not initialized yet.

The same problem happens for destructors if stream is destructed before x .

The nifty counter solution

It leverages the fact that in a TU objects are initialized and destructed in order. But we can't have a stream object for every TU, so we use a trick, we instead use a StreamInitializer object for every TU. 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 ();
}

Our app.cpp is the same

// 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

Now stream is not a value, it's a reference. So in this line Stream& stream = reinterpret_cast<Stream&> (stream_buf); no Stream object is created. Just the reference is bound to the still non existing object residing at the stream_buf memory. This object will be constructed later.

Every compilation unit has a streamInitializer object. Those objects initialize in any order, but it doesn't matter because on only one of them nifty_counter will be 0 and on that and only on that one the line new (&stream) Stream (); is executed. This line actually creates the stream object at the memory location stream_buf .

Now why is the line X x{}; ok in this scenario? Because this TU's streamInitializer object is initialized before x , which ensures that stream is initialized before x . This is true for every TU where stream.hpp is correctly included before any use of the stream object.

The same logic applies to the destructor, but in reverse order.

Recap

Static objects in different TU can be initialized in any order. This is a problem if one static object from one TU uses in it's constructor or destructor another static object from another TU.

In a TU object are constructed and destructed in order.

In the nifty connter idiom:

  • one TU holds a raw memory for the stream object
  • stream is a reference to the object at that memory location
  • a streamInitializer object is created in every every TU that uses the stream object.
  • only 1 streamInitializer will create the stream object, the 1st that is initialized
  • in a TU before every use of x at least the streamInitializer in that TU is already initialized. This means that at least one streamInitializer was constructed, this means that stream is constructed before the constructor of x .

include guards omitted for brevity

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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