Alexander Nasonov writes more on singleton.
Hi Alan,
I posted a reference to the Overload 76 to the C++ forum of Russian Software Developer Network ( http://www.rsdn.ru ) and I have had a few replies regarding my article. Since the article has not been reviewed, these comments can be considered as postmortem peer reviews.
An anonymous reader replied (translated from Russian):
Why did you say nothing about the most popular Meyers singleton where the LOCAL static variable is being used? You showed 2 most awful realizations, which nobody uses (I hope!) and it makes little sense to speak about them. I do not see any advantages of your realization over the Meyers singleton.
I agree that I should have discussed the Meyers singleton in the article. Single-threaded implementation is very simple and it manages dependencies automatically:
Singleton& instance() { static Singleton inst; return inst; }
But it is not so simple in a multithreaded program. Although the C++ standard does not define the term 'thread', my experience says that, if main() is already entered, a thread safety is often guaranteed for static objects at namespace scope but not for local static variables. As a result, the inst object may be initialized more than once if two threads call the instance() simultaneously.
A naive modification of the code above:
mutex mtx; Singleton& intance() { lock l(mtx); // lock mtx now, unlock in dtor static Singleton inst; return inst; }
would break a dependency tracking because now the instance() can't be called before the mtx is initialized. On POSIX platforms, it can be fixed by using PTHREAD_MUTEX_INITIALIZER to initialize the mtx object at static phase:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; Singleton& intance() { lock l(mtx); // lock mtx now, unlock in dtor static Singleton inst; return inst; }
Unfortunately, a static initializer for CRITICAL_SECTION is not available on Windows. One has to write a wrapper but it's not a trivial task.
This code can be optimized further using double locking technique but you should be very careful. Refer to [DCLocking] from the references section of the article.
Another anonymous reader wrote a test program to check a thread safety of the Meyers singleton:
template <typename T> class singleton_meyers { public: static T& instance() { static T obj; std::cout << "instance finished\n" ;; return obj; } }; struct sleep_in_ctor { sleep_in_ctor() { std::cout << "ctor started\n"; ; sleep(5); std::cout << "ctor finished\n"; } }; void stupid_func() { std::cout << "stupid func\n"; singleton_meyers<sleep_in_ctor> tmp; tmp.instance(); } int main() { boost::thread thrd1(stupid_func); boost::thread thrd2(stupid_func); thrd1.join(); thrd2.join(); }
Before doing thread-safety analysis, I'd like to note that this program uses I/O ( cout ) and process scheduling calls ( sleep ). In general, these calls should be avoided in tests that try to detect race conditions. The output is differ depending on the version of gcc it is compiled with.
Compiled with gcc 4.1 | Compiled with gcc 3.4 |
stupid func ctor started stupid func <<< 5 sec pause >>> ctor finished instance finished instance finished |
stupid func ctor started stupid func ctor started <<< 5 sec pause >>> ctor finished instance finished ctor finished instance finished |
As you see, gcc 4.1 correctly initializes the instance while gcc 3.4 incorrectly initializes two instances. Starting from version 4.0, gcc supports one-time construction API: http://www.codesourcery.com/cxx-abi/abi.html#once-ctor.
It is on by default but you can disable it with -fno-threadsafe-statics option. Note that it's not a portable extension and you shouldn't rely on it, though it's worth trying it out to detect recursive initialization (refer to 6.7 [stmt.dcl], bullet 4: If control re-enters the declaration (recursively) while the object is being initialized, the behaviour is undefined).
To summarize the reviews, I missed one important case which can be used in multithreaded programs if code is written properly, though it may be slower than a solution presented in the article because synchronization is required.