Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Static Initialization via Schwartz Counter

Tags:

c++

A Schwartz counter is intended to ensure that a global object is initialized before it is used.

Please consider the use of a Schwartz counter shown below.

File Foo.h:

class Foo
{
   Foo::Foo();
};

File Foo.cpp:

#include "Foo.h"

// Assume including Mystream.h provides access to myStream and that
// it causes creation of a file-static object that initializes
// myStream (aka a Schwartz counter).
#include "MyStream.h"

Foo::Foo()
{
   myStream << "Hello world\n";
}

If Foo::Foo() runs after main() starts, the use of myStream is guaranteed to be safe (i.e. myStream will have been initialized before use) because of the file-static initializer object mentioned in the comments.

However, suppose the Foo instance gets created before main() starts, as would happen if it were global. This is shown here:

File Global.cpp:

#include "Foo.h"

Foo foo;

Note that Global.cpp does not get a file-static initializer object like Foo.cpp does. In this case, how does the Schwartz counter ensure that the MyStream initializer (and therefore the MyStream object itself) are initialized before foo? Or can the Schwartz counter fail in this case?

like image 325
Dave Avatar asked Feb 12 '12 19:02

Dave


1 Answers

The use of "Schwartz counters" (so called after Jerry Schwartz who designed the basics of the IOStreams library as it is now in the standard; note that he can't be blamed for many of odd choices because these were tagged onto the original design) can result in accesses to objects before they are constructed. The most obvious scenario is calling a function during construction of a global object which calls into another translation unit using the its own global constructed via a Schwartz counter (I'm using std::cout as the global being guarded by a Schwartz counter to keep the example short):

// file a.h
void a();

// file a.cpp
#include <iostream>
void a() { std::cout << "a()\n"; }

// file b.cpp
#include <a.h>
struct b { b() { a(); } } bobject;

If the global objects in file b.cpp are constructed prior to those in the file a.cpp and if std::cout is constructed via a Schwartz counter with a.cpp being the first instance, this code will fail. There is are at least two other reasons why Schwartz counters don't work particularly well:

  1. When using a global object for this, this objects ends up being constructed twice. Although this works in practice when done right, I think this is ugly. A work-around for this is to use a char buffer of appropriate size for the actual definition of the object (these typically get mangled to the name as an object of the correct type). However, in both of these cases things are messy.
  2. When the global objects guarded by a Schwartz counter are used in many translation units (as it the case for std::cout) this may cause a significant start-up delay: well-written code typically doesn't use any global initialization but the Schwartz counter needs to run a piece of code for each of the object files which needs to be loaded.

Personally I have come to the conclusion that this technique is a nifty idea but it doesn't work in practice. There are three approaches I use instead:

  1. Don't use global objects. This make this whole discussion obsolete and works best especially in concurrent code. Where a global resource is absolutely needed, a function static object returned by reference and initialized using std::call_once() is a much better alternative.
  2. Placing the global object at an appropriate location when linking the executable (e.g. last) causes it to be initialized first. I had experimented with this in the past and back then I found that I can place object files appropriately on all systems I cared for. The main drawback here is that there are no guarantees and things might change when switching between compiler versions. For the C++ standard library this is, however, acceptable (and I only cared about the global stream objects when I did this).
  3. Put the global objects into a dedicated shared library: when the shared library is loaded, its initialization code is executed. The objects in the shared library become available only after the initialization is complete. I found that this works reliably but requires an extra library.
like image 114
Dietmar Kühl Avatar answered Nov 02 '22 08:11

Dietmar Kühl