2023-04-20

Yep. That was kinda dumb of me.

Consider the canonical Meyer's Singleton implemetaion in C++ (2011 or later standard): class Singleton { Singleton(); ~Singleton(); public: Singlton(const Singleton &) = delete; Singleton & operator=(const Singleton &) = delete; Singleton & instance() { static Singleton s; return s; } }; It has all the properties you might want of a Singleton:1 things like guaranteed single initialization and thread safety. Plus, it's easier to remember than any of the other functional variants I've seen.

But there is a subtle trap in the fact that I haven't shown you my implementation of the c'tor.2 One that I just tripped on in a big way. What happens if you were to call instance() in the constructor?

Now, it is unlikely that anyone will write Singleton::Singelton() { instance(); } and overlook the silliness of it for long after the program hangs during testing. However, writing Singleton::Singelton() { supportRoutine() } where supportRoutine() calls helperWidget() which calls instance() is a lot easier to miss for a few frustrating hours. Trust me.

This is about the object lifetime model in modern C++. And honsestly I didn't know enough about the hoary details to figure out what the standard requires when I realized what I had done, but MSVC 2019 appears to enter an infinite recursion. Luckily the internet is full of people who have more (or at least different) expertise than me, so I was able to get some help. The first source that my google-fu found was a blog post by mbedded.ninja which includes an explicit call-out to the standard3 telling us this is undefined behavior.

So, yeah. Don't do that.

Late addition: on reflection, the reason I got into that pickle in the first place was that I was using the Singleton both to manage an OS resource and and as a repository for utility code related to the resources. Having them in the same context made it too tempting to trigger some of the utility code from the c'tor and involke the recursion. Re-writing the code to better folloow the Single Responisibility Principle made the loop imnpossible in the first place and made me re-think when to invoke the utility code.


1 Assuming, of course, that you want a Singleton; more on that in another post.

2 I've left the d'tor unspecified as well for a philosophical reason: you should always decide on their implementaion as a pair. I don't want to commit to = default; for the d'tor until I know what I'm doing in the c'tor. In the instance that brought the subject up I was grabbing OS resources in the c'tor and so needed a non-trivial d'tor that would nicely give them back. Cue "but technically's" about the destruction order fiasco. I said I would discuss the (often ill-)advisability of Singletons in another post.

3 Section 6.7 paragraph 4 which says in part:

If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

No comments:

Post a Comment