Singleton Patterns are DANGEROUS (when used across the border of shared libraries)

Singleton Patterns are DANGEROUS

… When you want to control the order of initialization/deinitialization of your objects across shared libraries.

Consider the case where you have a beautiful logger class, which is a singleton, and another class which will put some information to log. It is also totally legit that you want to log when the object is being contructed and being destructed.

In the object, codes can be like following:

sth::sth(){
  logger::instance().log("sth is being constructed");
}
sth::~sth(){
  logger::instance().log("sth is being destructed");
}

// may be in other file, or same file

// This is also possible in case sth is actually a 
// factory and the polymorphic class is registered
// in following pattern
static auto whatever = sth::instance().do_something();

In this case, you might think that the order of initialization and deinitialization of the logger and the object is guaranteed. Since C++ standard seems to guarantee that for objects with static storage duration they are destructed as if std::atexit called is right after the completion of the constructor of the object to book the operation of destruction.

The finish of construction of logger is sequenced before the finish of construction of sth. That runtime should be destructing sth before logger.

Minimal Reproducible Example for the Issue

The point of this repository is to prove things can go wrong when you are doing this in a complexed project without properly specifying the dependency of shared libraries.

Just compile the project:

mkdir build && cd build && cmake .. && cmake --build . 

And run it:

It seems to be legal in C++ standard that the order don’t matter when things goes to beyond the boundary of shared libraries. There are some discussions here you can refer to.

What Happens in Runtime?

And when it comes to the implementation of glibc, things related to destruction and dynamic libraries are:

Now we are starting the program:

So, if we don’t properly specify the dependency of shared libraries, and the initialization of those local static objects are initialized from shared libraries, __run_exit_handlers will call _dl_fini before looking at those entries of global objects, and _dl_fini will lead to the destruction of the global objects in the order given by the dependency of shared libraries, not given by the order of initialization.

And if we did not specify the dependency of shared libraries properly, it becomes the order of linking. This leads to wrong order of destruction of global objects in some cases.

Recommendation if you really want to get the order right