How to Use std::monostate with std::variant in C++
This article explains how std::monostate functions in
modern C++ to solve default-construction limitations in
std::variant. You will learn why a
std::variant requires its first alternative to be
default-constructible, how std::monostate acts as a
lightweight placeholder to represent a deliberate "empty" or
uninitialized state, and how to implement and query this pattern
effectively in your code.
The Default
Construction Problem in std::variant
In C++17, std::variant is a type-safe union. When you
default-construct a std::variant, it automatically attempts
to default-construct its first alternative type:
#include <variant>
#include <string>
struct NonDefaultConstructible {
NonDefaultConstructible(int value) : id(value) {}
int id;
};
// This fails to compile:
// std::variant<NonDefaultConstructible, std::string> v;Because NonDefaultConstructible does not provide a
default constructor, the variant itself cannot be default-constructed.
Reordering types to put a default-constructible type first is not always
possible or logically desirable, especially if the variant represents a
domain model where none of the active data states are an appropriate
default.
What is std::monostate?
std::monostate is a utility struct defined in the
<variant> header. It is an empty type designed
specifically to serve as a well-behaved unit type:
struct monostate {};Because it is an empty struct, std::monostate:
- Has a trivial default constructor, copy constructor, and destructor.
- Occupies minimal size (1 byte by C++ object size rules, though often absorbed by alignment/padding in a variant).
- Implements all comparison operators (
==,!=,<, etc.), making a variant containing it comparable if the other alternatives are also comparable.
Representing the Empty State
By placing std::monostate as the first alternative in
the type list, you explicitly grant the variant a valid,
default-constructible state that represents "empty" or "no value":
#include <variant>
#include <iostream>
struct Connection {
Connection(std::string host) : endpoint(host) {}
std::string endpoint;
};
using ConnectionState = std::variant<std::monostate, Connection>;
int main() {
// Successfully default-constructs into the monostate alternative
ConnectionState state;
if (std::holds_alternative<std::monostate>(state)) {
std::cout << "State is currently uninitialized.\n";
}
// Transition to an active state
state = Connection("127.0.0.1:8080");
if (std::holds_alternative<Connection>(state)) {
std::cout << "Connected to: " << std::get<Connection>(state).endpoint << "\n";
}
// Explicitly reset back to empty
state = std::monostate{};
}Checking for the Empty State
You can query whether the variant is in its empty state using three primary techniques:
std::holds_alternative: Checks directly for the type.bool isEmpty = std::holds_alternative<std::monostate>(state);index()check: Inspects the active alternative index directly without specifying the type name. Sincestd::monostateis the first element, its index is0.bool isEmpty = (state.index() == 0);std::visit: Handles the empty condition naturally as part of pattern matching.std::visit([](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, std::monostate>) { std::cout << "Handling empty state\n"; } else { std::cout << "Handling active state\n"; } }, state);
When to Use
std::monostate vs. std::optional
- Use
std::optional<std::variant<Ts...>>when the "emptiness" is an orthogonal concern to the data types. This approach makes the absence of a value an explicit wrapper around an otherwise valid variant. - Use
std::variant<std::monostate, Ts...>when the empty state is an intrinsic part of the variant's state machine (such as a state transition system:Idlerepresented bystd::monostate,Connecting,Connected), or when you want to avoid wrapping the variant in another layer of template syntax.