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.
// 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
.
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.
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:
stream
is a reference to the object at that memory location streamInitializer
object is created in every every TU that uses the stream
object.streamInitializer
will create the stream object, the 1st that is initializedx
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.