Skip to content
C++
Idiom
since C++17
Basic

Monostate

All instances share state via static members (design pattern), and std::monostate is a C++17 unit type that makes variant default-constructible.

Monostatesince C++17

As a design pattern, Monostate makes every instance of a class share the same underlying state through static data members without restricting instantiation; as a library type, std::monostate (C++17, <variant>) is an empty unit type whose sole purpose is enabling std::variant to be default-constructible when its first alternative is not.

Overview

The name "Monostate" covers two distinct but conceptually related ideas in C++.

The Monostate design pattern predates modern C++ standards and offers an alternative to the Singleton. Where Singleton enforces a single instance, Monostate enforces a single state: every object reads and writes the same static storage, so the number of live instances is irrelevant. The public interface looks entirely ordinary—no getInstance() factory, no pointer plumbing. Callers can construct, copy, and destroy Monostate objects as if they were value types, while all of them silently alias the same global storage.

std::monostate borrows the name for a completely different purpose: it is a trivially default-constructible empty struct in <variant> that acts as a sentinel first alternative in std::variant. This solves a specific gap in the variant design: std::variant<T, U> is only default-constructible if T is default-constructible, because default construction value-initialises index 0. Putting std::monostate at index 0 gives the variant a guaranteed "uninitialised" state without requiring any of the concrete alternatives to be default-constructible.


The Monostate Design Pattern

Syntax

The pattern is mechanical: move every data member to static, leave the interface unchanged.

cpp
class Logger {
public:
    void setLevel(int lvl)       { level_ = lvl; }   // writes static storage
    int  getLevel() const        { return level_; }   // reads static storage
    void setSink(std::FILE* out) { sink_  = out; }
    void log(int severity, std::string_view msg);

private:
    // Pre-C++17: define these out-of-line in exactly one .cpp
    static int        level_;
    static std::FILE* sink_;
};

int        Logger::level_ = 0;
std::FILE* Logger::sink_  = stderr;

Since C++17, inline static replaces out-of-line definitions and keeps the initialiser co-located with the declaration:

cpp
class Logger {
    // ...
private:
    inline static int        level_ = 0;       // C++17
    inline static std::FILE* sink_  = stderr;  // C++17
};

Example

cpp
#include <cstdio>
#include <string_view>

class Logger {
public:
    void setLevel(int lvl)        { level_ = lvl; }
    int  getLevel() const         { return level_; }
    void setSink(std::FILE* out)  { sink_  = out; }

    void log(int severity, std::string_view msg) {
        if (severity >= level_)
            std::fprintf(sink_, "[%d] %.*s\n", severity,
                         static_cast<int>(msg.size()), msg.data());
    }

private:
    inline static int        level_ = 0;       // C++17
    inline static std::FILE* sink_  = stderr;  // C++17
};

void subsystemA() {
    Logger a;
    a.setLevel(2);
    a.log(1, "below threshold");  // suppressed — severity 1 < level 2
    a.log(3, "logged");
}

void subsystemB() {
    Logger b;
    // b sees level_ == 2 because subsystemA already set it
    b.log(2, "also logged — level_ is shared global state");
}

There is no registry, no shared pointer, and no GetInstance(). The coupling between subsystemA and subsystemB is completely invisible from the call sites—which is both the pattern's appeal and its primary hazard.


std::monostate (C++17)

Syntax

cpp
#include <variant>   // C++17

namespace std {
    struct monostate {};

    // All comparison operators; every instance compares equal to every other
    constexpr bool operator==(monostate, monostate) noexcept { return true;  }
    constexpr bool operator< (monostate, monostate) noexcept { return false; }
    constexpr bool operator> (monostate, monostate) noexcept { return false; }
    constexpr bool operator<=(monostate, monostate) noexcept { return true;  }
    constexpr bool operator>=(monostate, monostate) noexcept { return true;  }
    constexpr bool operator!=(monostate, monostate) noexcept { return false; }
    // C++20: constexpr std::strong_ordering operator<=>(monostate, monostate) noexcept

    // Specialisation for use in unordered containers (C++17)
    template<> struct hash<monostate>;
}

std::monostate has no data members, no user-declared constructors, and participates in all standard comparison operations. The C++20 spaceship operator (<=>) yields std::strong_ordering::equal for any two instances.

Example: deferred-initialisation resource

A common use case is a variant holding a resource that may not be acquired at construction time, alongside a "not yet connected" sentinel:

cpp
#include <cassert>
#include <stdexcept>
#include <string>
#include <variant>    // C++17

struct Database {
    Database() = delete;
    explicit Database(std::string conn) : conn_(std::move(conn)) {}
    void query(std::string_view sql) { /* ... */ }
    std::string conn_;
};

class App {
public:
    // Default-constructible because std::monostate is default-constructible
    App() = default;

    void connect(std::string conn) {
        db_ = Database{std::move(conn)};   // transitions to index 1
    }

    void run(std::string_view sql) {
        // std::holds_alternative is clearer than index() == 0
        if (std::holds_alternative<std::monostate>(db_))  // C++17
            throw std::runtime_error{"not connected"};
        std::get<Database>(db_).query(sql);
    }

private:
    std::variant<std::monostate, Database> db_;  // C++17; index 0 = "empty"
};

int main() {
    App app;
    // app.run("SELECT 1");  // throws std::runtime_error — still monostate

    app.connect("host=localhost dbname=prod");
    app.run("SELECT 1");  // ok — db_ now holds Database at index 1
}

std::monostate vs std::optional

std::variant<std::monostate, T> and std::optional<T> both model "value or nothing", but they serve different purposes:

  • std::optional<T> (C++17) is the right tool when you have exactly one possible concrete type.
  • std::monostate earns its place when the variant already carries two or more concrete alternatives and you need an empty sentinel at index 0. It also composes naturally with std::visit, where std::optional does not.
  • C++23 adds monadic operations (and_then, transform, or_else) to std::optional, widening the gap for single-alternative cases.

Best Practices

Monostate design pattern

  • Use it only when every conceivable caller genuinely wants the same shared configuration. The pattern breaks the moment any subsystem needs an independent instance.
  • Prefer inline static (C++17) over out-of-line definitions to prevent initialisation from being scattered across translation units.
  • Document the shared-state semantics at the class declaration. Nothing in the type signature warns callers that two Logger variables are aliases for the same global.

std::monostate

  • Put std::monostate first in the type list; the standard mandates that default construction initialises index 0, so index 0 must be default-constructible for the variant to be default-constructible.
  • Prefer std::holds_alternative<std::monostate>(v) over v.index() == 0—the former survives type-list reordering during refactoring.
  • If you only need "value or nothing" with a single concrete type, use std::optional<T> instead.

Common Pitfalls

Monostate design pattern

  • Silent mutation through copies: a function receiving a Logger by value can still mutate global state. Copy construction does not isolate state; it clones a handle that aliases the same storage.
  • Static initialisation order fiasco: when one Monostate class's static members depend on another's, initialisation order across translation units is undefined. inline static with constant initialisers avoids this; non-trivial initialisation should use a function-local static accessor pattern.
  • Thread safety: every read/write to static members is a potential data race. Either use std::atomic (C++11) for scalars or protect writes with a std::mutex.

std::monostate

  • std::get<T>(v) with the wrong alternative throws std::bad_variant_access at runtime. Always guard access with std::holds_alternative<T>(v) or use std::get_if<T>(&v) which returns a nullable pointer.
  • Accessing std::get<S>(v) on a default-constructed std::variant<std::monostate, S> throws immediately—index 0 holds monostate, not S. The example in cppreference makes this mistake intentionally to show the exception; in production code, check the index before every access or rely on std::visit.

std::monostate in Practice

Nullable variant — the canonical use case

The most common use of std::monostate is providing a guaranteed default state for a variant whose other alternatives are not default-constructible. Place it at index 0:

cpp
#include <variant>
#include <string>
#include <iostream>

// Neither int* nor std::string has a meaningful "absent" default here —
// we want an explicit "nothing chosen yet" state.
using Token = std::variant<std::monostate, int, std::string>;

Token parseToken(std::string_view src) {
    if (src.empty())      return {};           // default-constructs to monostate
    if (src[0] >= '0' && src[0] <= '9')
                          return std::stoi(std::string(src));
    return std::string(src);
}

int main() {
    Token t = parseToken("");

    // Two equivalent ways to check for the empty sentinel:
    if (std::holds_alternative<std::monostate>(t))  // idiomatic — survives reordering
        std::cout << "no token\n";
    if (t.index() == 0)                             // fragile — breaks if you move monostate
        std::cout << "no token (index check)\n";
}

Prefer std::holds_alternative<std::monostate>(v) over v.index() == 0. If you later insert another alternative before std::monostate in the type list, the index check silently gives wrong answers while holds_alternative still compiles correctly — and then fails to compile if std::monostate is removed entirely, which is the right outcome.

Default construction behaviour

std::variant's default constructor value-initialises the first alternative. If the first type is not default-constructible, the variant itself is not default-constructible:

cpp
struct NoDef { NoDef() = delete; NoDef(int) {} };

// std::variant<NoDef, int> v;  // ERROR: NoDef not default-constructible
std::variant<std::monostate, NoDef, int> v;  // OK: monostate at index 0

The standard guarantees std::monostate is trivially default-constructible, trivially copyable, trivially destructible, and constexpr-friendly. It carries no storage overhead beyond the discriminant the variant already maintains.

Handling std::monostate in std::visit

std::visit requires every alternative to be handled. std::monostate is just another type — give it a branch:

cpp
#include <variant>
#include <string>
#include <iostream>

using Value = std::variant<std::monostate, int, double, std::string>;

void print(const Value& v) {
    std::visit([](const auto& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, std::monostate>) {
            std::cout << "(empty)\n";
        } else if constexpr (std::is_same_v<T, int>) {
            std::cout << "int: " << arg << '\n';
        } else if constexpr (std::is_same_v<T, double>) {
            std::cout << "double: " << arg << '\n';
        } else {
            std::cout << "string: " << arg << '\n';
        }
    }, v);
}

The if constexpr chain is evaluated at compile time per instantiation, so there is no runtime overhead for the type discrimination.


std::monostate as a Type-Erased Empty Sentinel

std::monostate belongs to a small family of standard "unit types" — types that carry no data but have a distinct type identity:

TypeHeaderPurpose
std::monostate<variant>Empty sentinel alternative in std::variant
std::nullopt_t<optional>Tag type for "construct optional in empty state"
std::nullptr_t<cstddef>Type of the nullptr literal
std::in_place_t<utility>Tag for in-place construction

Each is a distinct type with exactly one value. They exist to participate in overload resolution and template specialisation without carrying any payload. std::monostate is hashed (via std::hash<std::monostate>) and totally ordered (all instances compare equal), making it usable as a key in ordered and unordered containers — a property that raw tag types often lack.

Heterogeneous variant registries

When building a heterogeneous registry or a state machine with an explicit "uninitialised" state, std::monostate gives that state a first-class type rather than an error-prone sentinel value:

cpp
#include <variant>
#include <vector>
#include <string>

struct Connected    { std::string host; };
struct Authenticated { std::string user; };
struct Streaming    { int streamId; };

// Uninitialised state has its own type — not an integer flag, not nullptr.
using SessionState = std::variant<
    std::monostate,   // index 0: not yet connected
    Connected,        // index 1
    Authenticated,    // index 2
    Streaming         // index 3
>;

bool isConnected(const SessionState& s) {
    return !std::holds_alternative<std::monostate>(s);
}

This makes illegal transitions detectable at runtime (and potentially at compile time with the right visitor structure), and eliminates the "magic number 0 means uninitialised" antipattern.


Monostate vs Singleton

Both patterns address the "single shared state" problem, but from opposite angles. The table below captures the key differences:

DimensionSingletonMonostate
How "oneness" is enforcedInstance count — only one object may existState sharing — any number of objects, but all alias the same storage
ConstructorPrivate; access via getInstance() factoryPublic; clients construct freely
InheritanceHard to subclass (private constructor)Subclassable; subclass adds static members or overrides behaviour
Copy / moveTypically deleted or returns the same instanceCopy construction creates a new handle to the same state (aliasing, not cloning)
Swap / resetMust go through getInstance()Requires resetting all static members individually
Test isolationReplace with a test double by injecting via pointer/referenceEqually hard — state is global; must manually reset statics between tests
Accidental creationExplicit design choiceCan happen by accident when a class accumulates too many static data members
Visibility of couplingObvious — callers must call getInstance()Hidden — two local Logger variables look independent but are not

When Monostate is accidentally created

The most dangerous form of the Monostate antipattern is the one nobody intended: a class that started as a proper value type but had static members added incrementally for "convenience" until all meaningful state ended up in global storage. Symptoms include:

  • Unit tests that pass individually but fail when run together (because static state leaks between tests).
  • Surprising behaviour when a library is used from multiple threads.
  • Inability to have two independent instances of what is conceptually a configurable resource (two loggers writing to different files, two connection pools with different limits).

If you find yourself in this situation, the remediation is to convert static data members back to instance members and inject configuration explicitly — either through the constructor or a dedicated configure() method.


Thread Safety of the Monostate Pattern

Because all instances of a Monostate class share the same static storage, concurrent access has exactly the same hazards as any other global mutable state.

Reads are safe if no concurrent writes occur. A read-only Monostate (all methods are const and only read static members) has the same thread safety as a global constant.

Writes require synchronisation. The idiomatic C++17 approach uses inline static std::mutex co-located with the data it protects:

cpp
#include <mutex>
#include <string>
#include <string_view>

class Config {
public:
    static void setEndpoint(std::string ep) {
        std::lock_guard lock{mutex_};   // C++17 CTAD
        endpoint_ = std::move(ep);
    }

    static std::string endpoint() {
        std::lock_guard lock{mutex_};
        return endpoint_;
    }

private:
    inline static std::mutex mutex_;
    inline static std::string endpoint_ = "localhost:8080";
};

// Usage from any instance — they all share the same mutex_ and endpoint_:
void configure() { Config{}.setEndpoint("prod.example.com:443"); }
void read()      { auto ep = Config{}.endpoint(); }

For scalar types, std::atomic is lighter-weight and avoids the mutex entirely:

cpp
#include <atomic>

class Stats {
public:
    void increment() { ++requests_; }
    long total() const { return requests_.load(std::memory_order_relaxed); }

private:
    inline static std::atomic<long> requests_{0};
};

Be aware of compound operations. If a method reads one static member and then writes another based on the result, you need to hold the mutex for the entire operation, not just each individual access. Acquiring per-member locks introduces TOCTOU races.

Static initialisation and thread safety

C++11 guarantees that function-local static variables are initialised exactly once, with full thread safety. inline static class members, however, are initialised before main() — they obey the static initialisation order rules, not the dynamic initialisation rules. For non-trivial inline static members, the order of initialisation across translation units is unspecified. If your Monostate class depends on another Monostate class's members being initialised first, you may hit the Static Initialisation Order Fiasco. The safest remedy is the Meyers Singleton accessor:

cpp
class Registry {
public:
    static std::vector<std::string>& entries() {
        static std::vector<std::string> data;  // initialised on first call, thread-safe (C++11)
        return data;
    }

    void add(std::string s) { entries().push_back(std::move(s)); }
    const std::vector<std::string>& all() const { return entries(); }
};

This gives the Monostate pattern the same lazy-initialisation guarantee as a Singleton while keeping the ordinary constructor interface.


Monostate, Borg, and Cross-Language Comparisons

The Monostate pattern is sometimes called the Borg pattern in Python communities, where it appears as:

python
class Borg:
    _shared_state = {}          # class-level dict — the single shared state
    def __init__(self):
        self.__dict__ = self._shared_state   # every instance shares the same __dict__

Every Python instance of Borg has its own identity (id(a) != id(b)) but shares the same attribute dictionary. The C++ analog uses static data members for the same effect. The Python version is considered a curiosity more than a recommendation; the same verdict applies to C++.

In Java, the equivalent is a class with only static fields and methods — sometimes called a "utility class" or "helper class". Java discourages instantiation of such classes by making the constructor private, which ironically makes them indistinguishable from the Singleton pattern in practice.

The takeaway for C++: both Singleton and Monostate are mechanisms for managing global mutable state. Choosing between them is usually a matter of API ergonomics. Neither solves the underlying architectural problem, which is that global mutable state makes code hard to test, reason about, and compose. Prefer injecting dependencies explicitly. Reserve the Monostate pattern for cases — like process-wide configuration or a global diagnostics sink — where the global nature is genuinely intentional and documented.


See Also

  • reference/idioms/algebraic-types — std::variant as a sum type; std::monostate appears as the canonical "empty" arm of algebraic product/sum encodings.
  • reference/idioms/lazy-initialization — frequently paired with the Monostate design pattern when static members require non-trivial setup that must be deferred.