C++ Nifty Counter idiom; why?

前提是你 提交于 2021-02-05 13:40:21

问题


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.


回答1:


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.




回答2:


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"; }



回答3:


All of your questions about utility / performance of Nifty Counter aka Schwartz Counter were basically answered by Maxim Egorushkin in this answer (but see also the comment threads).

Global variables in modern C++

The main issue is that there is a trade-off taking place. When you use Nifty Counter your program startup time is a bit slower (in large projects), since all these counters have to run before anything can happen. That doesn't happen in Meyer's singleton.

However, in the Meyer's singleton, every time you want to access the global object, you have to check if it's null, or, the compiler emits code that checks if the static variable was already constructed before any access is attempted. In the Nifty Counter, you have your pointer already and you just fire away, since you can assume the init happened at startup time.

So, Nifty Counter vs. Meyer's singleton is basically a trade-off between program startup time and run-time.




回答4:


With the solution you have here, the global stream variable gets assigned at some point during static initialization, but it is unspecified when. Therefore the use of stream from other compilation units during static initialization may not work. Nifty counter is a way to guarantee that a global (e.g. std::cout) is usable even during static initialization.

#include <iostream>

struct use_std_out_in_ctor
{
    use_std_out_in_ctor()
    {
        // std::cout guaranteed to be initialized even if this
        // ctor runs during static initialization
        std::cout << "Hello world" << std::endl;
    }
};

use_std_out_in_ctor global; // causes ctor to run during static initialization

int main()
{
    std::cout << "Did it print Hello world?" << std::endl;
}


来源:https://stackoverflow.com/questions/36844393/c-nifty-counter-idiom-why

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!