Skip to content
C++
Idiom
since C++98
Intermediate

Nifty Counter

A header-based reference-counting technique that guarantees a static object is initialized before any translation unit that includes its header uses it.

Nifty Countersince C++98

A header-embedded reference-counting pattern that constructs a shared static object exactly once β€” on first inclusion β€” and destroys it after the last including translation unit is torn down, eliminating static initialization order dependence without requiring callers to change anything.

Overview

The Static Initialization Order Fiasco (SIOF) is one of C++'s most persistent hazards. When a static object in one translation unit (TU) depends on a static object in another TU at construction time, the standard provides no ordering guarantee. The nifty counter idiom β€” sometimes called the Schwarz counter β€” solves this entirely within the library: the header plants a sentinel object in every including TU that drives initialization and shutdown of the shared resource.

The standard library relies on this technique directly. std::ios_base::Init, pulled in by every <iostream> inclusion, is the canonical nifty counter that keeps std::cin, std::cout, std::cerr, and std::clog alive for any TU that touches them.

Why the naive approach fails

cpp
// logger.hpp β€” naive, broken
extern Logger& gLogger;  // defined in logger.cpp

// module_a.cpp
#include "logger.hpp"
static Foo foo;  // Foo's constructor calls gLogger.log(...)
                 // SIOF: gLogger may not be initialized yet

The relative initialization order of gLogger (in logger.cpp) and foo (in module_a.cpp) is unspecified by the standard. It may work in debug builds and silently corrupt memory in release, or crash only when the link order changes.

Syntax

The idiom splits across exactly two files: a header that every client includes, and a single .cpp that owns the storage.

logger.hpp:

cpp
#pragma once
#include <string_view>

class Logger {
public:
    void log(std::string_view msg);
};

// Single shared reference; storage lives in logger.cpp
extern Logger& gLogger;

// Anonymous namespace gives each TU its own instance of the
// sentinel without symbol collisions at link time.
namespace {
    struct LoggerInit {
        LoggerInit();
        ~LoggerInit();
    };
    // Initialized before any static declared after this #include in the same TU.
    static LoggerInit loggerInitInstance;
}

logger.cpp:

cpp
#include "logger.hpp"
#include <new>

// Plain int β€” zero-initialized during constant initialization,
// which precedes all dynamic initialization. This ordering is guaranteed.
static int loggerRefCount = 0;

// alignas ensures placement new doesn't produce UB. C++11 alignas.
// std::aligned_storage (C++11) was deprecated in C++23; prefer this form.
alignas(Logger) static unsigned char loggerStorage[sizeof(Logger)];

Logger& gLogger = reinterpret_cast<Logger&>(loggerStorage);

LoggerInit::LoggerInit() {
    if (loggerRefCount++ == 0)
        ::new (&gLogger) Logger{};  // placement new; valid since C++98
}

LoggerInit::~LoggerInit() {
    if (--loggerRefCount == 0)
        gLogger.~Logger();          // explicit destructor call; valid since C++98
}

void Logger::log(std::string_view msg) { /* ... */ }

The zero-initialization of loggerRefCount is the load-bearing guarantee. Plain scalars with static storage duration are constant-initialized before any dynamic initialization fires, so the counter is reliably 0 when the first LoggerInit constructor runs regardless of which TU gets there first.

Examples

Shared event bus with multi-TU subscribers

cpp
// event_bus.hpp
#pragma once
#include <functional>
#include <mutex>
#include <string>
#include <vector>

class EventBus {
public:
    using Handler = std::function<void(const std::string&)>;  // C++11
    void subscribe(std::string topic, Handler h);
    void publish(const std::string& topic, const std::string& payload);
private:
    std::mutex mu_;                                            // C++11
    std::vector<std::pair<std::string, Handler>> handlers_;
};

extern EventBus& gBus;

namespace {
    struct EventBusInit { EventBusInit(); ~EventBusInit(); };
    static EventBusInit eventBusInitInstance;
}
cpp
// event_bus.cpp
#include "event_bus.hpp"
#include <new>

static int refCount = 0;
alignas(EventBus) static unsigned char storage[sizeof(EventBus)];  // C++11
EventBus& gBus = reinterpret_cast<EventBus&>(storage);

EventBusInit::EventBusInit() { if (refCount++ == 0) ::new (&gBus) EventBus{}; }
EventBusInit::~EventBusInit() { if (--refCount == 0) gBus.~EventBus(); }

void EventBus::subscribe(std::string topic, Handler h) {
    std::lock_guard lock{mu_};                                      // C++17 CTAD
    handlers_.emplace_back(std::move(topic), std::move(h));
}

void EventBus::publish(const std::string& topic, const std::string& payload) {
    std::lock_guard lock{mu_};                                      // C++17
    for (auto& [t, h] : handlers_)                                  // C++17 structured bindings
        if (t == topic) h(payload);
}

Any TU that includes event_bus.hpp can safely use gBus from its own static constructors:

cpp
// audio_system.cpp
#include "event_bus.hpp"

static struct AudioSystem {
    AudioSystem() {
        // Safe: eventBusInitInstance ran before this constructor
        // because it was declared earlier in this TU via the header.
        gBus.subscribe("audio/mute", [](const std::string& v) { /* ... */ });
    }
} audioSystem;

Comparing with the Meyers singleton (C++11)

C++11 made function-local statics thread-safe, enabling the simpler Meyers singleton:

cpp
Logger& getLogger() {
    static Logger instance;  // C++11: initialization guaranteed thread-safe
    return instance;
}

Prefer this when call sites can tolerate a function call. The nifty counter is the right choice when the object's name must be a variable reference (std::cout, gLogger, gBus) rather than a function call β€” whether for API stability, macro compatibility, or standard-library-style conventions.

C++17 inline variables β€” partial relief

C++17 inline variables permit a variable definition in a header without ODR violations:

cpp
// C++17
inline constexpr int kMaxRetries = 3;           // constant-initialized; no SIOF
inline const std::string kServiceName = "svc";  // dynamic init β€” SIOF risk remains

Inline variables eliminate SIOF only for types with trivial or constexpr constructors. For non-trivially-constructible globals, the nifty counter remains necessary.

Best Practices

  • Always alignas the raw buffer. An unaligned char array causes undefined behavior on placement new for any type with alignment greater than 1. alignas(T) unsigned char buf[sizeof(T)] is the correct form in all standards. Avoid std::aligned_storage, deprecated in C++23.
  • Keep refCount as a plain int. Static initialization is effectively single-threaded from the perspective of user code (it precedes main and user-created threads in well-behaved programs). std::atomic adds unnecessary overhead and doesn't protect against the actual SIOF-era race, which the counter design already prevents.
  • Use an anonymous namespace for the sentinel. This gives each TU a privately-linked LoggerInit instance while guaranteeing they all reference the same gLogger from logger.cpp. A named namespace or file-scope static would risk ODR violations or duplicate symbols.
  • Include the owning .cpp in the sentinel's TU count. Because logger.cpp itself includes logger.hpp, it also gets a LoggerInit instance. This means refCount starts at 1 before any other TU's instance fires. This is benign and correct β€” the last destructor to run will still reduce the count to 0.
  • Prefer the Meyers singleton for new code when naming flexibility is not required. The nifty counter is the correct technique for library-style APIs where a global reference is part of the contract.

Common Pitfalls

Omitting alignas on the storage buffer

cpp
// Undefined behavior β€” char has alignment 1; Logger may require alignment 8 or more
static unsigned char loggerStorage[sizeof(Logger)];
cpp
// Correct (C++11)
alignas(Logger) static unsigned char loggerStorage[sizeof(Logger)];

Making the reference inline

If you write inline Logger& gLogger = ... in the header, every TU gets its own Logger object rather than sharing one. The extern declaration in the header paired with the definition in the .cpp is essential.

Using the object from a thread spawned during static initialization

Nifty counter guarantees ordering within the static initialization phase, not across threads spawned during that phase. If another static object's constructor starts a thread that touches gLogger before the including TU's LoggerInit constructor has run, the access is still a race. This is a broader SIOF variant the idiom cannot eliminate.

Double-inclusion concern

The anonymous namespace ensures loggerInitInstance has internal linkage, so multiple inclusions of the header in the same TU (via #pragma once or include guards) produce only one sentinel instance per TU. Without #pragma once or guards, double inclusion would double-increment the counter, causing premature or deferred destruction.

The Static Initialization Order Fiasco in Depth

The static initialization order fiasco (SIOF) is not a compiler bug or a linker defect β€” it is a direct consequence of the C++ standard deliberately leaving the initialization order of dynamic-duration statics across translation units unspecified. Understanding why the standard does this, and exactly where the guarantee boundaries sit, is essential before choosing a remedy.

What the standard guarantees β€” and what it does not

Within a single translation unit, static objects with dynamic initialization are constructed in the order their definitions appear in the source file. This is a firm guarantee. Destruction happens in the exact reverse order.

Across translation units, no such guarantee exists. The standard treats each TU's initialization as an atomic unit but says nothing about the relative order of those units. In practice the order is determined by the linker β€” often the order in which object files appear on the command line or in a static archive β€” but this is entirely implementation-defined and can change between build configurations, build systems, or toolchain versions.

The fiasco materializes when a static object's constructor, running in TU A, reads or calls into a static object defined in TU B, but TU B has not yet been initialized:

cpp
// config.hpp
extern Config gConfig;          // defined in config.cpp

// network.cpp
#include "config.hpp"

static NetworkManager gNetwork; // constructor reads gConfig.listenPort()
                                 // SIOF: gConfig may be zero-initialized
                                 // raw memory at this point

The result is undefined behavior. On many platforms the read returns zero-initialized memory (e.g., listenPort() returns 0), which masks the bug in test environments where link order happens to be favorable. The crash emerges in production when the link order shifts β€” perhaps because a new library was added.

Zero-initialization is not dynamic initialization

A subtle but load-bearing detail: objects with static storage duration go through two phases. First, zero-initialization sets all bytes to zero before any user code runs β€” this is guaranteed to complete before dynamic initialization begins. Second, dynamic initialization runs the user-defined constructors.

Plain scalars like int refCount = 0 are satisfied entirely by zero-initialization; no dynamic initialization is needed. This is why the nifty counter's refCount can be read safely by the very first LoggerInit constructor: it has been reliably zeroed by the runtime before any dynamic initializer fires, regardless of TU order. Types with constexpr constructors are similarly promoted to constant initialization and escape the SIOF entirely.

How std::cout Uses the Nifty Counter

The nifty counter is not merely an idiom from textbooks β€” it is load-bearing infrastructure in every conforming C++ standard library. The mechanism centers on std::ios_base::Init.

std::ios_base::Init β€” the canonical nifty counter

Every standard library header that exposes std::cout, std::cin, std::cerr, or std::clog is required by the C++ standard ([iostream.objects.overview]) to ensure those objects are initialized before they are used. The mechanism mandated in practice is:

Including <iostream> causes the compiler to see a declaration of std::ios_base::Init and a static instance of it β€” call it __ioinit β€” placed in the including translation unit. Implementations typically arrange this through a line equivalent to:

cpp
// Inside <iostream> (simplified conceptual representation)
#include <ios>     // brings in ios_base::Init declaration
static std::ios_base::Init __ioinit;

Because __ioinit is a static object declared in the header, every TU that includes <iostream> gets its own copy. The reference counter in Init's implementation tracks how many of these instances exist:

  • When the first Init object is constructed (counter goes from 0 to 1), the streams std::cout, std::cin, std::cerr, and std::clog are constructed via placement new into their respective aligned storage buffers.
  • Each subsequent Init constructor increments the counter without doing further construction.
  • Each Init destructor decrements the counter; when it reaches 0 (the last including TU is torn down), the four stream objects have their destructors called explicitly, flushing buffers and releasing resources.

This guarantees that as long as any TU that includes <iostream> is alive and being executed, the standard streams are valid. A TU that includes <iostream> but never calls std::cout still contributes an Init instance, keeping the streams alive for other TUs that do use them.

Why placement new is mandatory for the streams

If std::cout were declared as a global std::ostream object directly in the implementation file, it would be a candidate for dynamic initialization. Its constructor could run either before or after any given TU's Init constructor, reintroducing the SIOF. Placement new into a zero-initialized aligned buffer bypasses this: the buffer itself is constant-initialized (it is a plain array of bytes, zero-initialized before dynamic init begins), and the stream object is only constructed at a precisely controlled point β€” inside the first Init constructor.

cpp
// Conceptual standard library implementation (not actual source)
alignas(std::ostream) unsigned char __cout_buf[sizeof(std::ostream)];
std::ostream& cout = reinterpret_cast<std::ostream&>(__cout_buf);

// In ios_base::Init constructor:
if (init_cnt++ == 0) {
    ::new (&cout) std::ostream(&cout_streambuf); // placement new
    ::new (&cerr) std::ostream(&cerr_streambuf);
    // ... cin, clog ...
}

The reference cout is valid from the moment the translation unit loads (it refers to the buffer, which is zero-initialized), but the ostream object layered on top of it is only live after the first Init fires. Code that accesses cout before including <iostream> bypasses this guarantee and has undefined behavior.

Full Implementation Walk-through

The following is a production-quality nifty counter implementation with annotations explaining each decision.

cpp
// stream.hpp β€” the public header every client includes
#pragma once
#include <cstddef>   // size_t

class Stream {
public:
    void write(const char* msg);
    void flush();
    ~Stream();
private:
    // implementation details hidden
};

// The shared global reference. extern: declared here, defined once in stream.cpp.
// Clients use this name directly, just like std::cout.
extern Stream& gStream;

// The sentinel type. Definition appears in the header so every TU that
// includes this header gets its own static instance.
struct StreamInit {
    StreamInit() noexcept;
    ~StreamInit() noexcept;
};

// Anonymous namespace: each TU gets a privately-linked StreamInit object.
// The anonymous namespace prevents ODR violations while ensuring that every
// TU's StreamInit constructor and destructor participate in the ref count.
namespace {
    // "static" is redundant inside an anonymous namespace (already internal
    // linkage) but some teams add it for emphasis. Both forms are correct.
    StreamInit streamInitSentinel;
}
cpp
// stream.cpp β€” the single definition unit
#include "stream.hpp"
#include <new>       // ::operator new (placement form)

// Zero-initialized during constant initialization β€” guaranteed to be 0
// before any dynamic initializer in any TU runs.
static int niftyCounter = 0;

// Aligned raw storage. alignas(Stream) is required: without it, placement
// new into a char array produces UB for types whose alignment exceeds 1.
// std::aligned_storage was deprecated in C++23; this pattern is preferred.
alignas(Stream) static unsigned char streamStorage[sizeof(Stream)];

// The reference binds to the storage region. The Stream object does not
// exist here yet β€” the buffer is zero-initialized memory. The object is
// constructed by the first StreamInit constructor below.
Stream& gStream = reinterpret_cast<Stream&>(streamStorage);

StreamInit::StreamInit() noexcept {
    // niftyCounter was zero-initialized before this runs, so the first
    // translation unit to include stream.hpp constructs the object.
    if (niftyCounter++ == 0)
        ::new (static_cast<void*>(&gStream)) Stream();
    // All subsequent TUs increment the counter and return.
}

StreamInit::~StreamInit() noexcept {
    // The last StreamInit to be destroyed decrements the counter to 0
    // and explicitly invokes the destructor. This is the only correct way
    // to destroy an object constructed via placement new.
    if (--niftyCounter == 0)
        gStream.~Stream();
}

void Stream::write(const char* msg) { /* ... */ }
void Stream::flush()               { /* ... */ }
Stream::~Stream()                  { flush(); }

Why niftyCounter must not be std::atomic

The reference count is deliberately a plain int, not std::atomic<int>. Static initialization happens before main() runs and before any user-created threads. In a conforming program, no thread races with the static initializers β€” threads only come into existence after the static initialization phase concludes (or, more precisely, after the thread creating them is initialized). Adding atomic would pay synchronization cost that is never needed and would create a false impression that the nifty counter is safe to use for objects that must be accessible during thread construction, which it is not.

Modern Alternatives

C++11 and later standards introduced features that eliminate SIOF for many use cases with less complexity than the nifty counter.

Meyers singleton β€” function-local static (C++11)

The simplest modern replacement is the function-local static, sometimes called the Meyers singleton after Scott Meyers who popularized it:

cpp
// logger.hpp
class Logger { public: void log(const char*); };

Logger& getLogger() {
    static Logger instance; // C++11: initialization is thread-safe and lazy
    return instance;
}

The C++11 standard ([stmt.dcl]) guarantees that if multiple threads simultaneously reach the initialization of a block-scope static variable, initialization occurs exactly once and all threads see the completed object. No explicit synchronization is required. The object is destroyed in reverse order of construction relative to other function-local statics.

Limitation: Call sites must use getLogger().log(msg) instead of gLogger.log(msg). This is fine for new APIs but cannot match an existing global-variable interface without source-level changes throughout the codebase.

constinit β€” enforced constant initialization (C++20)

For objects whose initial value can be computed at compile time, constinit instructs the compiler to perform constant initialization:

cpp
// C++20
constinit int gMaxConnections = 100;           // constant init; no SIOF ever
constinit thread_local int tLocalCache = 0;   // also works with thread_local

constinit is a specifier, not a type qualifier. It asserts that the variable is constant-initialized; if the initializer requires dynamic initialization, the declaration is ill-formed (a compile error). It does not imply const β€” the variable can be modified after initialization.

constinit solves SIOF completely for applicable types, but it requires a constexpr-evaluable initializer. Complex objects with heap allocations, file handles, or other runtime-dependent state cannot use it.

std::call_once β€” one-time initialization for complex objects (C++11)

When initialization is expensive, conditional, or can fail and be retried, std::call_once provides explicit one-time semantics:

cpp
#include <mutex>

class DatabasePool {
public:
    static DatabasePool& instance() {
        std::call_once(initFlag_, []{ pool_.emplace(/*config*/); });
        return *pool_;
    }
private:
    static std::once_flag initFlag_;
    static std::optional<DatabasePool> pool_;
};

Unlike function-local statics, std::call_once allows the initialization logic to be separated from the type definition and can be invoked conditionally. It also handles the case where initialization throws: the flag is not set on exception, so the next caller retries. This is a double-edged sword β€” retrying on failure is sometimes correct and sometimes masks bugs.

Comparison: Initialization Strategies

StrategyMin standardGlobal name?Thread-safe initDestruction order controlledComplexity
Nifty counterC++98YesYes (pre-main)Yes (last-out destroys)High
Meyers singletonC++11No (function call)Yes (guaranteed)Reverse of constructionLow
constinitC++20YesN/A (compile-time)Trivial (no dtor)Low
std::call_onceC++11No (via accessor)YesManual or staticMedium
Pointer leakC++98YesNoNever destroyedLow
inline globalC++17YesNo (SIOF risk)Unspecified across TUsLow

Notes on the table:

  • "Global name" means client code can write gStream.write(msg) without a function call.
  • The "pointer leak" pattern (static T* p = new T()) intentionally never destructs β€” acceptable only for objects that must outlive all static destructors.
  • inline globals (C++17) eliminate ODR issues but do not fix SIOF for types with dynamic initializers.

When to Still Use the Nifty Counter

Given that Meyers singleton is simpler and constinit eliminates SIOF at compile time, the nifty counter occupies a narrowing but irreplaceable niche:

The object's identity must be a variable reference. Standard library streams (std::cout) are variables by specification. Changing them to function calls would break decades of user code. Any library that has shipped a global-variable API and cannot change call sites is in the same position.

Destruction must happen at a well-defined point relative to other TUs. The Meyers singleton is destroyed in reverse order of construction, but its destruction point is relative to other function-local statics in the same TU β€” not necessarily after all users of the object across TUs have finished. With the nifty counter, the owning .cpp's sentinel is the last object alive (because it was among the first constructed), giving the implementation explicit control over when the destructor fires.

You need both construction and destruction order across TUs. constinit solves construction order for constant-initializable types but has nothing to say about destruction. The nifty counter's destructor is the only standard-compliant mechanism that guarantees a shared dynamic object is destroyed after the last TU that uses it is torn down.

Embedding in a library with a stable ABI. If a library header must work with consumers compiled under different standards (C++11, C++14, C++17, C++20) without recompilation of the consumers, the nifty counter's C++98 compatibility is an asset. constinit requires C++20; call_once requires C++11.

When none of these constraints apply β€” which is most application code written today β€” prefer the Meyers singleton for its simplicity and the strong thread-safety guarantee from the compiler.

See Also

  • reference/idioms/lazy-initialization β€” function-local statics as the simpler SIOF workaround when a function call is acceptable
  • reference/idioms/monostate β€” an alternative global-state pattern using a class with only static members
  • reference/idioms/counted-pointer β€” reference counting applied to heap object lifetime rather than static initialization order
  • std::ios_base::Init β€” the standard library's own nifty counter implementation, specified since C++98