Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Nifty Counter idiom; why?

I recently came across the Nifty Counter Idiom. My understanding is that this is used to implement globals in the standard library like cout, cerr, etc. Since the experts have chosen it, I assume that it's a very strong technique.

I'm trying to understand what the advantage is over using something more like a Meyer Singleton.

For example, one could just have, in a header file:

inline Stream& getStream() { static Stream s; return s; } static Stream& stream = getStream(); 

The advantage is you don't have to worry about reference counting, or placement new, or having two classes, i.e. the code is much simpler. Since it's not done this way, I'm sure there's a reason:

  1. Is this not guaranteed to have a single global object across shared and static libraries? It seems like the ODR should guarantee that there can only be one static variable.
  2. Is there some kind of performance cost? It seems like in both my code and the Nifty Counter, you are following one reference to get to the object.
  3. Is there some situations where the reference counting is actually useful? It seems like it will still just lead to the object being constructed if the header is included, and destroyed at program end, like the Meyer Singleton.
  4. Does the answer involve dlopen'ing something manually? I don't have too much experience with that.

Edit: I was prompted to write the following bit of code while reading Yakk's answer, I add it to the original question as a quick demo. It's a very minimal example that shows how using the Meyer Singleton + a global reference leads to initialization before main: http://coliru.stacked-crooked.com/a/a7f0c8f33ba42b7f.

like image 312
Nir Friedman Avatar asked Apr 25 '16 15:04

Nir Friedman


2 Answers

The static local/Meyer's singleton + static global reference (your solution) is nearly equivalent to the nifty counter.

The differences are as follows:

  1. No .cpp file is required in your solution.

  2. Technically the static Steam& exists in every compilation unit; the object being referred to does not. As there is no way to detect this in the current version of C++, under as-if this goes away. But some implementations might actually create that reference instead of eliding it.

  3. Someone could call getStream() prior to the static Stream& being created; this would cause difficulty in destruction order (with the stream being destroyed later than expected). This can be avoided by making that against the rules.

  4. The standard is mandated to make creating the static Stream local in the inline getStream thread safe. Detecting that this is not going to happen is challenging for the compiler, so some redundant thread-safety overhead may exist in your solution. The nifty counter does not support thread safety explicitly; this is considered safe as it runs at static initialization time, prior to when threads are expected.

  5. The call to getStream() must occur in each and every compilation unit. Only if it is proven that it cannot do anything may it be optimized out, which is difficult. The nifty counter has a similar cost, but the operation may or may not be be simpler to optimize out or in runtime cost. (Determining this will require inspecting resulting assembly output on a variety of compilers)

  6. "magic statics" (statics locals without race conditions) where introduced in C++11. There could be other issues prior to C++11 magic statics with your code; the only one I can think of is someone calling getStream() directly in another thread during static initialization, which (as mentioned above) should be banned in general.

  7. Outside the realm of the standard, your version will automatically and magically create a new singleton in each dynamicly linked chunk of code (DLL, .so, etc). The nifty counter will only create the singleton in the cpp file. This may give the library writer tighter control over accidentally spawning new singletons; they can stick it into the dynamic library, instead of spawning duplicates.

Avoiding having more than one singleton is sometimes important.

like image 179
Yakk - Adam Nevraumont Avatar answered Oct 02 '22 07:10

Yakk - Adam Nevraumont


Summarizing the answers and comments:

Let's compare 3 different options for a library, wishing to present a global Singleton, as a variable or via a getter function:

Option 1 - the nifty counter pattern, allowing the use of a global variable that is:

  • assured to be created once
  • assured to be created before the first usage
  • assured to be created once across all shared objects that are dynamically linked with the library creating this global variable.

Option 2 - the Meyers singleton pattern with a reference variable (as presented in the question):

  • assured to be created once
  • assured to be created before the first usage

However, it will create a copy of the singleton object in shared objects, even if all shared objects and the main are linked dynamically with the library. This is because the Singleton reference variable is declared static in a header file and must have its initialization ready at compile time wherever it is used, including in shared objects, during compile time, before meeting the program they will be loaded to.


Option 3 - the Meyers singleton pattern without a reference variable (calling a getter for retrieving the Singleton object):

  • assured to be created once
  • assured to be created before the first usage
  • assured to be created once across all shared objects that are dynamically linked with the library creating this Singleton.

However, in this option there is no global variable nor inline call, each call for retrieving the Singleton is a function call (that can be cached on the caller side).

This option would look like:

// libA .h struct A {     A(); };  A& getA();  // some other header A global_a2 = getA();  // main int main() {     std::cerr << "main\n"; }  // libA .cpp - need to be dynamically linked! (same as libstdc++ is...) // thus the below shall be created only once in the process A& getA() {     static A a;     return a; }   A::A() { std::cerr << "construct A\n"; } 
like image 28
Amir Kirsh Avatar answered Oct 02 '22 07:10

Amir Kirsh