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.

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