Skip to content

91 Advanced The Hundred

The Singleton That Died First

the singleton is lazy, thread-safe, and already gone

struct Logger {
    bool open = true;
    ~Logger() { open = false; }
    void write(const char* msg) {
        std::cout << (open ? "[log] " : "(logger already closed) ") << msg << "\n";
    }
};

Logger& logger() {
    static Logger instance;   // Meyers singleton: the classic init-order fix
    return instance;
}

struct Connection {
    Connection() { std::cout << "connection opened\n"; }
    ~Connection() { logger().write("connection closed"); }
};
Connection conn;

int main() { logger().write("app started"); }

Run it. Does the closing message reach the log?

Answer

No — the singleton is already gone. Typical output (GCC 11 on x86-64, -O0); rebuild with GCC at -O2 and the last line quietly turns back into [log] connection closed:

connection opened
[log] app started
(logger already closed) connection closed

Why

Statics are destroyed in reverse order of initialization, and a function-local static counts as initialized where control first passes its declaration: conn before main starts, instance inside it — last in, first dead. (GCC constant-initializes instance into .data, but it is still destroyed as if it had been initialized right there.) ~Connection then calls logger(), whose guard variable is never reset, so it hands back the corpse; reading open from a dead object is undefined behavior. At -O0 the byte still holds the destructor's false; from -O1 up GCC deletes that store, since nothing may legally read a member once the destructor has returned, and the check stops catching (Clang 18 keeps it). Log from Connection's constructor too and the bug vanishes, which is why it lands the day a log line gets deleted. -Wall -Wextra say nothing; Clang's -Wexit-time-destructors names both statics.

The fix

Make the singleton immortal — leak it inside logger(), so the atexit queue never sees it:

static Logger* instance = new Logger();   // never destroyed, so never too early
return *instance;                         // Valgrind: "still reachable", not a leak

Takeaway: you can force initialization order; destruction order you can only outlive.

Try it: g++ -std=c++17 -Wall -Wextra main.cpp -o demo && ./demo — then again at -O2.

Open in Compiler Explorer ↗ Quiz this entry