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 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 are one more: lambdas.
Please do not forget the big gotcha: Internal linkage and anonymous namespace’s.
So you are thinking now: What is this post? Reminding us of the whole of standard text? Is that it?
Well, no my dear readers. Humble myself has prepared yet another delicious little nugget for you to readily copy-paste into your code. 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 |
#pragma once /* (c) 2018 - 2021 by DBJ.ORG Important: this is in some header.h Very deliberately not in a anonymous namespace */ namespace dbj { // some arbitrary singleton class called "singleton" struct singleton final { // probably you might delete all the constructors here }; // it is made through this "factory method" inline singleton & instance() { // create single_instance here and now // and only once static singleton single_instance = [&]() -> singleton { // this is anonymous lambda // called only once // do some more complex initialization // here return {}; }(); // call immediately 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 |
Please observe carefully the above:
- the big gotcha first: anonymous namespace implies default static linkage for variables inside. Thus do not put this code inside it. This is the header code. This
dbj_once
namespace deliberately has a name.
If this would be anon. namespace variables inside will be treated as if they are static. Which will make things to 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 another 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 using std::call_once ? Or if you are WIN32 aficionado, why no just use InitOnceExecuteOnce()
a function? These two btw. are clearly illustrating what do I mean by “it was much more involved”, in them 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 here 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 is on a Godbolt.
Enjoy, study and come back to us with questions. But.
We need to talk about the beast
And that is modern C. It keeps churning out simpler, cleaner, faster and easier to maintain solutions.
How would one solve the above conundrum using modern C? Let us push you into the deep end of the pools:
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 56 57 58 59 60 |
/* Modern Singleton -- still of dubious value, but always in a high demand Altohugh modern C solution is much simpler and easier to maintain (c) 2018 - 2021 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 hiding // low level HW queries #define THIS_MONITOR_ID 13 #define INVALID_MONITOR_ID -1 // in C we keep the data separate // we do not mix it with behaviour typedef struct computer_ { struct monitor_ { int id_; } monitor; } computer; /* yes this is legal, and computer_ argument will never be null try this: monitor_runtime_id(0); and you will get this: warning: null passed to a callee that requires a non-null argument [-Wnonnull] */ int monitor_runtime_id(computer computer_[static 1]) { if (computer_[0].monitor.id_ > INVALID_MONITOR_ID) return THIS_MONITOR_ID; return INVALID_MONITOR_ID; } // global native pointer is instant fear!? // plese calm down and follow the code static computer* this_computer_ = 0; // once at startup __attribute__((constructor)) static void describe_this_computer(void) { if (this_computer_) return; this_computer_ = calloc(1, sizeof(computer)); this_computer_->monitor.id_ = THIS_MONITOR_ID; } // once at exit __attribute__((destructor)) static void leaving_this_computer() { if (!this_computer_) return; free(this_computer_); } 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.
Here is the mandatory Godbolt.