Just how many times you had admitted to yourself? It was rather foolish to think of globals in C++ as “a simple thing” :) After all: “What could possibly go wrong”tm ? If you know what I am talking about please proceed.
As it turns out many things can and will go wrong, with a singleton in particular. Specifically with what you might think of, as C++ global variables and functions and other “things”. I am sure every now and then, you are wondering why is this singleton not a singleton, or why is this function called more than once, and how it is, that nobody seems to care.
After many years of various coding idioms, that handle static linkage and global variables, and the famous “Singleton Pattern” implementations, may I be so bold to remind you: you do not have to dance around these issues anymore.
That was 2018 after all
As from C++11, you had a new memory model to enjoy. And from C++17 you had them inline variables too.
Since the C++ primordial soup, you have local statics. Aka static local variables. And as if this would be an end to this list of crucial C++ ingredients, there is one more ingredient in this secret sauce: lambdas.
Detour: Please do not forget the big gotcha: Internal linkage and anonymous namespace.
So you are thinking now: What is this post? Reminding us of the whole of the standard text? Is that it?
Well, no, my dear readers. Your humble servant aka myself has prepared yet another delicious little nugget for you to readily copy-paste into your code.
Lambda based singleton
Here it comes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// https://godbolt.org/z/nv5vbPcnq #pragma once #include <type_traits> /* (c) 2018 - 2023 by DBJ.ORG Important: Very deliberately not in an anonymous namespace */ namespace dbj_once { // some arbitrary singleton class called "singleton" struct singleton final { // some arbitrary payload bool made_once ; }; // try and use only default ctors // non default ctors are c++ design mistake static_assert( std::is_default_constructible_v<singleton> ); static_assert( std::is_trivially_default_constructible_v<singleton> ); static_assert( std::is_nothrow_default_constructible_v<singleton> ); // Singleton is made through this "factory method" inline singleton & instance() { // create single_instance here and now // and only once // as a result of inner lambda static singleton single_instance = [&]() -> singleton { // this is anonymous lambda // called only once // if need be, do some more // complex initialization here return { true }; }(); // call immediately and return single_instance; }; // not necessary, but one can save it in a reference // with C++17 inline variable declaration inline singleton & singleton_instance = instance(); } // dbj_once /* usage example int main (void) { return dbj_once::singleton_instance.made_once ; } */ |
Please observe carefully the above:
- the big gotcha first and again: anonymous namespace implies default static linkage for variables inside. Thus do not put that code inside it. This is the header code. This
dbj_once
namespace deliberately has that name.
If this would be anon. namespace variables inside will be treated as if they are static. Which will make things be called as many times as you have compilation units. Aka “cpp files”.
Effectivelyinline singleton & instance()
, will be linked as:static singleton & instance().
Please make sure to devote some time to the “Internal linkage” and anon. namespace’s subject. - Lock-Free. Since C++11, this is safe in presence of multiple threads (MT) and is supported as such by all major compilers. No
std::mutex
or some other kind of locking is necessary. - Call Once Mechanism. Inside
instance()
is an anonymous lambda that is guaranteed to be called only once. - This coding idiom is safe to use in header-only situations. Very important for modern and standard C++.
Please do remember: until C++11, to achieve the same, this was much more involved. Much more.
Now (one might ask) why not just use std::call_once ? Or if you are a WIN32 aficionado, why not just use InitOnceExecuteOnce()
a function? If you really need to please do try. But be forewarned these two are clearly illustrating what I mean by “it was much more involved”, in the bad old days before C++11.
You can, but you do not have to use the legacy singleton idioms. It is as simple as that. The code presented above is standard C++ and does it all, for you.
Bonus
Let us think. Compile-time singleton would be much more “singleton logical”. Unique things are unlikely to come and go dynamically at runtime. They can but are unlikely.
Computer parts represented as singletons are one example. Monitor
as a singleton instance is unlikely to disappear at runtime. But. It can.
So let’s code following those requirements. User view would be this:
1 2 3 4 5 6 |
// it is compile time AND a const reference constexpr auto const & compiletime_singleton = computer::monitor_singleton_instance ; // check monitor is not missing assert( compiletime_singleton.invalid_id != compiletime_singleton.id() ); |
Fully functional and working code of compile time Singleton is ready for you, on a Godbolt.
Enjoy, study, and come back to us with questions. But.
But wait, I just changed my mind. Completely.
We need to talk about the “beast”. And that “beast” is modern C. It keeps churning out simpler, cleaner, faster, and easier-to-maintain solutions. V.s. anything basically.
How would one solve the above conundrum using modern C? Let us push you into the deep end of the pool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
/* Modern Singleton -- still of dubious value, but always in a high demand Although modern C solution is much simpler and easier to maintain (c) 2018 - 2023 by DBJ.ORG */ #include <assert.h> #include <stdlib.h> #if __STDC_VERSION__ < 199901L #error Please use C99 or better #endif // these hash defines will be coming in some other way // can you think of some? #define THIS_MONITOR_ID 13 #define INVALID_MONITOR_ID -1 typedef struct computer_ { struct monitor_ { int id_; } monitor; } computer; int monitor_runtime_id(computer computer_ ) { return computer_.monitor.id_ ; } static computer this_computer_ = {{ INVALID_MONITOR_ID }}; // once at startup __attribute__((constructor)) static void describe_this_computer(void) { if (this_computer_.monitor.id_ != INVALID_MONITOR_ID) return; // make new computer struct on the stack "forgetting" the old this_computer_ = (computer){{ THIS_MONITOR_ID }}; this_computer_.monitor.id_ = THIS_MONITOR_ID; } // once at exit __attribute__((destructor)) static void leaving_this_computer() { if (this_computer_.monitor.id_ == INVALID_MONITOR_ID) return; this_computer_.monitor.id_ = INVALID_MONITOR_ID; } int main(void) { // check monitor is not missing assert(INVALID_MONITOR_ID != monitor_runtime_id(this_computer_)); return 42 ; ; } |
That is one simple and easy-to-maintain solution. From consistent requirements to a consistent solution.
In C, one produces programs, not abstractions from which programs might be built.
C executables are fast, small, and resilient. I prefer C.
Here is the mandatory Godbolt.