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++17As 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.
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:
class Logger {
// ...
private:
inline static int level_ = 0; // C++17
inline static std::FILE* sink_ = stderr; // C++17
};Example
#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
#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:
#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::monostateearns 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 withstd::visit, wherestd::optionaldoes not.- C++23 adds monadic operations (
and_then,transform,or_else) tostd::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
Loggervariables are aliases for the same global.
std::monostate
- Put
std::monostatefirst 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)overv.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
Loggerby 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
staticmembers depend on another's, initialisation order across translation units is undefined.inline staticwith constant initialisers avoids this; non-trivial initialisation should use a function-localstaticaccessor pattern. - Thread safety: every read/write to
staticmembers is a potential data race. Either usestd::atomic(C++11) for scalars or protect writes with astd::mutex.
std::monostate
std::get<T>(v)with the wrong alternative throwsstd::bad_variant_accessat runtime. Always guard access withstd::holds_alternative<T>(v)or usestd::get_if<T>(&v)which returns a nullable pointer.- Accessing
std::get<S>(v)on a default-constructedstd::variant<std::monostate, S>throws immediately—index 0 holdsmonostate, notS. The example in cppreference makes this mistake intentionally to show the exception; in production code, check the index before every access or rely onstd::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:
#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:
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 0The 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:
#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:
| Type | Header | Purpose |
|---|---|---|
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:
#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:
| Dimension | Singleton | Monostate |
|---|---|---|
| How "oneness" is enforced | Instance count — only one object may exist | State sharing — any number of objects, but all alias the same storage |
| Constructor | Private; access via getInstance() factory | Public; clients construct freely |
| Inheritance | Hard to subclass (private constructor) | Subclassable; subclass adds static members or overrides behaviour |
| Copy / move | Typically deleted or returns the same instance | Copy construction creates a new handle to the same state (aliasing, not cloning) |
| Swap / reset | Must go through getInstance() | Requires resetting all static members individually |
| Test isolation | Replace with a test double by injecting via pointer/reference | Equally hard — state is global; must manually reset statics between tests |
| Accidental creation | Explicit design choice | Can happen by accident when a class accumulates too many static data members |
| Visibility of coupling | Obvious — 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:
#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:
#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:
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:
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::variantas a sum type;std::monostateappears as the canonical "empty" arm of algebraic product/sum encodings.reference/idioms/lazy-initialization— frequently paired with the Monostate design pattern whenstaticmembers require non-trivial setup that must be deferred.