DEV Community

Singletons in C++

Paul J. Lucas on July 04, 2023

Introduction If exactly one instance of some class is needed by your program, a singleton is often used as a seemingly reasonable soluti...
Collapse
 
lewisxy profile image
Lewis

Hello, thank you for providing such a detailed introduction to Singleton. I went through your code and did some experiments. However, I suspect that the generalized nifty counter does not work (or maybe I missed something). The problem is here.

// Stream.h

class Stream : private singleton_counter {
    //...
};

extern Stream &stream;
static singleton_init<Stream> const stream_init{ &stream };

// Stream.cpp

static singleton_buf<Stream> stream_buf;
Stream &stream = stream_buf.ref();
Enter fullscreen mode Exit fullscreen mode

Notice that we are initializing variable stream_init using external reference stream in the header file (which is included by every .cpp files using stream, including Stream.cpp). Because we initialize stream in Stream.cpp. It's possible that stream has not been initialized (i.e., bind to the buffer) at the time we are using it to construct stream_init, since the order of construction of static objects are not well defined across translation unit. In such case, stream will refer to nullptr and the program will crash with segfault during placement new.

While the code works fine out of the box, to confirm my suspicion that it works due to luck, I added an output statement to the ref() method in singleton_buf while still maintaining its constexpr status, and the program crashed with segfault as expected.

    constexpr T& ref() noexcept {
        std::cerr << "ref" << std::endl;  // added this line
        return reinterpret_cast<T&>(buf_);
    }
Enter fullscreen mode Exit fullscreen mode

It also failed if I don't modify the function body, but remove the constexpr qualification on ref(). So maybe the original code happened to work due to some compiler optimization (that I am not sure whether it's backed by the C++ standard).

Note that I am using C++17 (macOS, clang 15.0, CLion default configuration), so I replaced concept <Singleton T> with <typename T, typename = std::enable_if_t<std::is_base_of_v<SingletonCounter, T>. But this shouldn't affect the test.

Collapse
 
pauljlucas profile image
Paul J. Lucas • Edited

Yes, I know that the order of construction of static objects is undefined across TUs. Perhaps this was another way the original code was also wrong.

I've made 3 changes:

  1. Changed std::byte to char (necessary for #2).
  2. Changed the constructor to:

    constexpr singleton_buf() : _buf{ 0 } { }
    

    (necessary for #3).

  3. Added constinit:

    static constinit singleton_buf<Stream> stream_buf;
    

See if that helps. Unfortunately, there's no way to make ref() be constexpr since reinterpret_casts aren't allowed in constexpr functions.

Collapse
 
lewisxy profile image
Lewis

The modification I proposed that breaks the code is actually my fault, print things in constexpr is not allowed by the standard, and my compiler is lenient enough to allow it. But stricter compiler would have throw compiler errors. I am not sure why standard does not allow reinterpret_cast in the constexpr function (perhaps due to potential UBs).

I did a bit more research and experimentation. The original code does work because reinterpret_cast<Stream&>(stream_buf) is actually a compile time value. There is no problem for compiler to compute the address of that static buffer and assign this value to the stream. This is vaguely mentioned in the standard as "constant initialization" instead of "zero initialization". You can clearly see from the compiler explorer that the value is set to the address vs zero. godbolt.org/z/PdThb59z5

What worries me is that gcc and clang disagree on whether to use "constant initialization" vs "zero initialization" in many cases (from the above compiler explorer link). This means such behavior is more or less implementation defined and possible not portable. But both of them agree on using "constant initialization" for the case in the original code. MSVC is a bit weird and I did not test it.

Thread Thread
 
pauljlucas profile image
Paul J. Lucas

For some reason, I never got a notification of your reply. I just stumbled across it today. Anyway....

I'd need to see the relevant section of the standard to know whether it's implementation-defined or not. If not, then either gcc or clang is wrong and I'd hope that they'd eventually fix the bug. I'd guess that it's not implementation-defined, i.e., is explicitly specified by the standard since it seems like too-important a detail.

Collapse
 
daniel_anderson_e7681f319 profile image
Daniel Anderson

Hello, Thank you for your time to explain how to implement the singleton «à la» cout. very instructive, and could not agree more with you: singletons should be use sparingly!
Now I have a problem with MSVC 2022, I cannot use

static constinit singleton_buf<Stream> stream_buf;
Stream &stream = stream_buf.ref();
Enter fullscreen mode Exit fullscreen mode

as the stream reference is not initialized and singleton_init receive a nullptr.

using std::byte, and removing const does the trick
as in:

alignas(Stream) static std::byte stream_buf[ sizeof(Stream) ];
Stream &stream = reinterpret_cast<Stream&>( stream_buf );
Enter fullscreen mode Exit fullscreen mode

So I'm not sure that even if GCC and Clang do the compile initialization, maybe the standard does not mandate it ?
I'm compiling without optimization, with optimization there is no problems!

Collapse
 
pauljlucas profile image
Paul J. Lucas

Are you saying my original code compiles and works with optimization?

Collapse
 
daniel_anderson_e7681f319 profile image
Daniel Anderson

without optimisation, the init is called before the reference memory has been bound to the reference, so it crashes.
With optimization, MSVC does init the reference to the proper block of memory at compile time, so the init receive a proper pointer!
Also, your reinterpret_cast is not good as it cast away constness of the byte array.
Why in the first place do you declare the byte array const ? maybe I'm missing something

Collapse
 
thebuzzsaw profile image
Kelly Brown

Good tutorial. Now never ever ever ever ever use it.

Collapse
 
pauljlucas profile image
Paul J. Lucas

You did read the “Specific Cases for Singletons” section, right?

Collapse
 
thebuzzsaw profile image
Kelly Brown

Yes. That is a good section. It also largely neuters much of the reason people want to use singletons. I've generally leaned the direction of "we just don't need singletons anymore", and it hasn't hurt me. Sure, I'll consume a singleton if I have to. I can't control what other people do, but I'm not going to add to the singleton dumpster fire.

Collapse
 
dinhluanbmt profile image
dinhluanbmt • Edited

I really like the template

template<Singleton T>
class singleton_init
Enter fullscreen mode Exit fullscreen mode

but i just wonder do we need to handle the copy constructor and assignment operator ?

Collapse
 
pauljlucas profile image
Paul J. Lucas

Actually, they should be handled for all the classes the same way: forbid them. Fixed. Thanks.

Collapse
 
nicola_gelmini_82 profile image
Nicola Gelmini

Hi, thank you for this really interesting article, I'm developing in C++17 and unfortunately I have to work with a singleton and unfortunately I can't use concepts. I'm completely inexperienced with the semantics of concepts, I can be called a youngling :p, is there a way to define the Singleton template in a similar way to what you defined using concepts, but in a C++17 compliant way?

thank you Master

Collapse
 
pauljlucas profile image
Paul J. Lucas

You just use typename instead. Concepts only add additional constraints to types (which is a good thing), but plain typename will still work.