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:

  1. std::holds_alternative: Checks directly for the type.

    bool isEmpty = std::holds_alternative<std::monostate>(state);
  2. index() check: Inspects the active alternative index directly without specifying the type name. Since std::monostate is the first element, its index is 0.

    bool isEmpty = (state.index() == 0);
  3. 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: Idle represented by std::monostate, Connecting, Connected), or when you want to avoid wrapping the variant in another layer of template syntax.