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.

See Also

  • reference/idioms/algebraic-typesstd::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.