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.
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.