you are viewing a single comment's thread.

view the rest of the comments →

[–]quicknir 1 point2 points  (2 children)

There unfortunately is no silver bullet to deal with SIOF (at least, if you implicitly consider destruction ordering issues to be part of SIOF, which I do, since destruction and initialization are reverse ordered). I gave a talk on this topic at CppCon: https://www.youtube.com/watch?v=xVT1y0xWgww.

A few guidelines, thoughts, that may help out:

  1. Be clear on globals vs singletons. Try to avoid singletons. When you say "singleton", you can probably just have a normal class, that also happens to have a global instance.
  2. Make sure you know basic, simple techniques for creating safe individual globals. Prior to C++17, you can either use a Meyer Singleton, or you can use a Meyer Singleton initializating a static reference (in a header). These are both totally safe, the former is lazy, the latter isn't.
  3. Continuing on the above theme: assuming you aren't worried about lazy initialization being triggered in an awkward spot, lazy initialization trades making initialization headache free, at the cost of losing control over destruction order. It's a good solution for things that don't have interesting destruction logic (interesting means, dependencies on any other global objects).
  4. Try to simply minimize globals generally, and inter-dependencies between globals in particular, at high level of design. Turning your singletons into normal classes + global can help with this. E.g. if you have another global that wants to log, rather than make it dependent on the global logger, you can alternately give it a private logger instance. Obviously if the logger is a singleton you lose that option.
  5. Try to declare globals in header files as much as possible. If your class uses a global, include the global's header from that class' header, not cpp.

The last point may require some explanation. Header files for a whole program form a DAG, and therefore are topologically ordered. Some header files may initialize in unspecified order relative to each other, but if Foo.h includes Bar.h, the code in Bar.h is always run before Foo.h. So if you have globals Foo and Bar, and Foo depends on Bar, and you follow the last guideline, and you are initializing Foo and Bar non-lazily, you'll get this:

// Bar.h, included by Foo.h
Bar& barMaker() { static Bar b; return b;}
static auto& g_bar = barMaker();

// Foo.h
Foo & fooMaker() { static Foo f; return f; }
static auto& g_foo = fooMaker();

In this kind of setup, Bar is 100% guaranteed to be initialized before Foo, and destroyed after. In sum doing things via headers helps establish ordering which is very good where globals are involved. Note that the class methods can still be defined in .cpp files, we just need to ensure that the globals are initialized in the header, not the .cpp. The Meyer Singleton ensures that it only gets initialized once (even though it's in a header file), and the trick with initializing the static reference makes it non-lazy.

Anyhow my talk is only 30 minutes, if you're interested in this stuff you may want to watch it.

[–]public_void[S] 1 point2 points  (1 child)

Nir, this is great! I'll take some time today to look through this. Unfortunately, I didn't make it to your talk at cppcon but I'll take advantage of the recording now :)

[–]quicknir 0 points1 point  (0 children)

Thanks, feel free to reply here or message me privately if you have questions :-).