OSC++.030: std::variant — Type-Safe Alternatives

Dark neon-green technical illustration showing multiple C++ value types flowing into a type-safe variant container and dispatching to matching handlers.

std::variant is a C++ standard-library type that stores exactly one value from a fixed list of possible types. If a program needs a value that can legally be an int, double, or std::string, a declaration such as std::variant<int, double, std::string> expresses that design directly. Unlike a traditional union, the variant also tracks which alternative is currently active, making it a type-safe discriminated union.

Mike Shah introduces std::variant as a type-safe tagged union in modern C++.

The type was added in C++17 through the <variant> header. The cppreference std::variant reference documents the current interface, while the original WG21 P0088R3 proposal describes the design goal: a type-safe union that makes the active alternative explicit and queryable.

Declaring and Assigning a Variant

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, double, std::string> value;

    value = 42;
    value = 3.14;
    value = std::string{"server-ready"};
}

A default-constructed variant contains its first alternative when that type can be default constructed. In the example above, value begins as an int containing zero. Later assignments replace the active alternative. This differs from OSC++.029 on std::optional: an optional models whether one type exists, while a variant models which one of several known types is active.

Checking the Active Type

std::variant<int, double, std::string> value = 42;

if (std::holds_alternative<int>(value)) {
    std::cout << "integer\n";
}

std::cout << "active index: " << value.index() << '\n';

std::holds_alternative<T> asks whether the variant currently contains T. The index() member reports the zero-based alternative index. For std::variant<int, double, std::string>, index 0 means int, index 1 means double, and index 2 means std::string. Prefer type-based checks when the type itself communicates the design more clearly than an integer index.

Reading Values with get and get_if

std::variant<int, double, std::string> value = std::string{"ready"};

std::cout << std::get<std::string>(value) << '\n';

if (auto* text = std::get_if<std::string>(&value)) {
    std::cout << *text << '\n';
}

std::get<T> returns the stored value when T is active, but throws std::bad_variant_access when the requested type is not active. std::get_if<T> is the non-throwing alternative: it returns a pointer to the value when the type matches and nullptr otherwise. The exception behavior connects directly with OSC++.015 on exception handling.

CppCon examines how std::variant can replace some forms of runtime polymorphism.

Using std::visit

std::visit is the central dispatch mechanism for variant-based code. Instead of manually checking every possible type, a visitor provides behavior for the active alternative. This can make state machines, protocol messages, parsed values, configuration data, and other closed sets of types easier to model.

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, double, std::string> value = 3.14;

    std::visit([](const auto& item) {
        std::cout << item << '\n';
    }, value);
}

The generic lambda above works because each listed type can be streamed with operator<<. More complex visitors can use overloaded lambdas or function objects to give each alternative different behavior. That approach is related to, but fundamentally different from, the virtual-function design introduced in OSC++.018 on runtime polymorphism: variant dispatch operates over a closed, compile-time list of possible types.

Andrei Alexandrescu explores variant layout, construction, destruction, and performance implications at CppCon.

std::monostate and Default Construction

Sometimes none of the meaningful alternatives can be default constructed, or a program needs an explicit “nothing selected yet” state. std::monostate is a lightweight placeholder type for that purpose. A declaration such as std::variant<std::monostate, Config, Error> makes the first state intentionally empty-like while preserving variant semantics.

struct Config {
    int port;
};

struct Error {
    std::string message;
};

std::variant<std::monostate, Config, Error> state;

The valueless_by_exception State

A variant normally holds exactly one alternative, but the standard permits a rare valueless_by_exception state if replacing one alternative with another throws during certain operations. Production code that stores types with throwing move or construction behavior should understand this possibility. The earlier lessons on move operations and exception guarantees are especially important when variants contain resource-owning classes.

Recent C++ community benchmarking shows that std::visit performance can vary substantially across standard-library implementations and compiler generations.

Compile and Run

g++ -std=c++17 -Wall -Wextra -pedantic variant_demo.cpp -o variant_demo
./variant_demo

The -std=c++17 flag is required because std::variant entered the standard library in C++17. Newer language modes such as C++20, C++23, or C++26 also provide the type, with later standards extending constexpr support and visitation capabilities.

Exercise

Create a program named sensor_value.cpp with std::variant<int, double, std::string>. Store an integer temperature reading, a floating-point voltage, and a string status one at a time. For each value, use std::visit to print both the value and a label describing its type. Then add one test using std::get_if so the program safely checks for a string without throwing.

Knowledge Check

  1. What problem does std::variant solve?
  2. What does holds_alternative<T> test?
  3. What is the difference between std::get and std::get_if?
  4. What does std::visit do?
  5. Why might std::monostate be placed first in a variant?
  6. What does valueless_by_exception() report?

Answers

  1. It stores one value from a fixed set of possible types while tracking the active type safely.
  2. It reports whether the variant currently contains the requested type.
  3. std::get returns the requested alternative and can throw on a mismatch; std::get_if returns a pointer or nullptr.
  4. It invokes a callable with the currently active alternative.
  5. It provides an explicit harmless default or empty-like state.
  6. It reports whether the variant currently holds no alternative because an exception interrupted a state-changing operation.

BitcoinVersus.Tech Editor’s Note:
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support to help further secure the integrity of our research initiatives, please donate using the QR code provided

Bitcoin donation QR code for 5,000 sats.

For best QR-code scanning results, please scan this donation code from a desktop or larger screen. Mobile screens may prevent camera recognition.

One response to “OSC++.030: std::variant — Type-Safe Alternatives”

  1. […] lesson follows OSC++.030: std::variant and OSC++.029: std::optional. Those two vocabulary types deliberately communicate more structure to […]

    Like

Leave a Reply