Nifty/Schwarz counter, standard compliant?

2019-01-23 04:32发布

问题:

I had a discussion this morning with a colleague about static variable initialization order. He mentioned the Nifty/Schwarz counter and I'm (sort of) puzzled. I understand how it works, but I'm not sure if this is, technically speaking, standard compliant.

Suppose the 3 following files (the first two are copy-pasta'd from More C++ Idioms):


//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 ** ) { ... }

... and here lies the problem! There are two static variables:

  1. "nifty_counter" in Stream.cpp; and
  2. "initializer" in Program.cpp.

Since the two variables happen to be in two different compilation units, there is no (AFAIK) official guarantee that nifty_counter is initialized to 0 before initializer's constructor is called.

I can think of two quick solutions as two why this "works":

  1. modern compilers are smart enough to resolve the dependency between the two variables and place the code in the appropriate order in the executable file (highly unlikely);
  2. nifty_counter is actually initialized at "load-time" like the article says and its value is already placed in the "data segment" in the executable file, so it is always initialized "before any code is run" (highly likely).

Both of these seem to me like they depend on some unofficial, yet possible implementation. Is this standard compliant or is this just "so likely to work" that we shouldn't worry about it?

回答1:

I believe it's guaranteed to work. According to the standard ($3.6.2/1): "Objects with static storage duration (3.7.1) shall be zero-initialized (8.5) before any other initialization takes place."

Since nifty_counter has static storage duration, it gets initialized before initializer is created, regardless of distribution across translation units.

Edit: After rereading the section in question, and considering input from @Tadeusz Kopec's comment, I'm less certain about whether it's well defined as it stands right now, but it is quite trivial to ensure that it is well-defined: remove the initialization from the definition of nifty_counter, so it looks like:

static int nifty_counter;

Since it has static storage duration, it will be zero-initialized, even without specifying an intializer -- and removing the initializer removes any doubt about any other initialization taking place after the zero-initialization.



回答2:

I think missing from this example is how the construction of Stream is avoided, this often is non-portable. Besides the nifty counter the initialisers role is to construct something like:

extern Stream in;

Where one compilation unit has the memory associated with that object, whether there is some special constructor before the in-place new operator is used, or in the cases I've seen the memory is allocated in another way to avoid any conflicts. It seems to me that is there is a no-op constructor on this stream then the ordering of whether the initialiser is called first or the no-op constructor is not defined.

To allocate an area of bytes is often non-portable for example for gnu iostream the space for cin is defined as:

typedef char fake_istream[sizeof(istream)] __attribute__ ((aligned(__alignof__(istream))))
...
fake_istream cin;

llvm uses:

_ALIGNAS_TYPE (__stdinbuf<char> ) static char __cin [sizeof(__stdinbuf <char>)];

Both make certain assumption about the space needed for the object. Where the Schwarz Counter initialises with a placement new:

new (&cin) istream(&buf)

Practically this doesn't look that portable.

I've noticed that some compilers like gnu, microsoft and AIX do have compiler extensions to influence static initialiser order:

  • For Gnu this is: Enable the init-priority with the -f flag and use __attribute__ ((init_priority (n))).
  • On windows with a microsoft compiler there is a #pragma (http://support.microsoft.com/kb/104248)