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

Dark neon technical illustration showing int, double, and string values entering a type-safe C++ variant and being dispatched to matching handlers.

Elementary Overview

std::variant is a C++17 standard-library type that stores exactly one value from a fixed list of possible types. A declaration such as std::variant<int, double, std::string> value; says that value may contain an int, a double, or a std::string, but only one of them is active at a time. This makes std::variant a type-safe alternative to a traditional C-style union when the program needs several possible representations for one logical value.

The type lives in the <variant> header. Its central design idea is a closed set of alternatives: the compiler knows every permitted type in advance. That differs from runtime polymorphism through virtual functions, where new derived classes may be added later. The complete library interface is documented in the cppreference reference for std::variant.

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

Creating and Assigning Alternatives

If the first alternative is default-constructible, a default-constructed variant starts by holding that first type. For example, std::variant<int, std::string> data; begins with an int initialized to zero. Assigning data = std::string{"ready"}; changes the active alternative to std::string. Assigning data = 42; changes it back to int.

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

int main() {
    std::variant<int, double, std::string> data = 42;

    data = 3.14;
    data = std::string{"online"};

    std::cout << "Active index: " << data.index() << '\n';
}

The index() member reports which alternative is active. Index zero refers to the first declared type, index one to the second, and so on. Code usually should not depend heavily on raw numeric indexes because type-based access is clearer, but the index is useful for diagnostics, serialization, and low-level state inspection.

C++ community discussion comparing std::variant-based state modeling with ordinary enums and runtime polymorphism.

Reading the Active Value

std::get<T>(variant) retrieves the active value when the variant currently holds type T. If the requested type is not active, std::get throws std::bad_variant_access. A safer inspection pattern is to call std::holds_alternative<T> first or use std::get_if<T>, which returns a pointer to the active value when the type matches and nullptr otherwise.

std::variant<int, std::string> state = std::string{"running"};

if (std::holds_alternative<std::string>(state)) {
    std::cout << std::get<std::string>(state) << '\n';
}

if (const auto* text = std::get_if<std::string>(&state)) {
    std::cout << "Length: " << text->size() << '\n';
}

This explicit checking is closely related to the absence-handling model introduced in OSC++.029 on std::optional. The difference is that std::optional<T> represents either one T or no value, while std::variant<A, B, C> represents exactly one value selected from several different types.

std::visit and Type-Safe Dispatch

std::visit applies a callable to the currently active alternative. The compiler checks whether the visitor can handle the possible variant alternatives. A generic lambda can handle every type through one function body, while an overloaded visitor can give each type its own behavior. This turns the variant into a compact form of compile-time-checked runtime dispatch.

Mike Shah demonstrates the visitor pattern using std::variant and std::visit.
std::variant<int, double, std::string> value = 12.5;

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

For different behavior per type, a common helper combines several lambdas into one overload set:

template<class... Ts>
struct overloaded : Ts... {
    using Ts::operator()...;
};

template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

std::variant<int, double, std::string> value = std::string{"rack-07"};

std::visit(overloaded{
    [](int n) { std::cout << "int: " << n << '\n'; },
    [](double x) { std::cout << "double: " << x << '\n'; },
    [](const std::string& s) { std::cout << "string: " << s << '\n'; }
}, value);
Pacific++ demonstrates how std::variant can model type-safe state machines and move certain errors from runtime to compile time.

std::monostate and Default Construction

A variant normally needs its first alternative to be default-constructible if the variant itself is default-constructed. std::monostate is an empty placeholder type that can be placed first when an explicit neutral state is useful. For example, std::variant<std::monostate, Connected, Failed> can begin in a known “not yet initialized” state before transitioning to a real application state.

This is different from ownership tools such as the smart pointers introduced in OSC++.021. A variant chooses among value types. A smart pointer primarily describes ownership and lifetime of dynamically managed objects. Both can participate in polymorphic designs, but they solve different problems.

The valueless_by_exception State

A variant is designed to hold one active alternative, but a rare exceptional case exists. During certain type-changing operations, construction of the replacement value can throw after the previous value has already been destroyed. The variant can then become valueless_by_exception(). In that state, index() returns std::variant_npos. Good type design, non-throwing moves, and careful exception guarantees reduce the chance of reaching this state.

The connection to OSC++.028 on std::move_if_noexcept is important. Both topics show how move operations and exception guarantees affect whether library containers can preserve strong invariants while changing stored objects.

When std::variant Fits

std::variant fits best when the set of legal alternatives is known and intentionally closed: parser tokens, protocol messages, configuration values, state-machine states, syntax-tree nodes, or a result that can take several specific forms. It is less attractive when new runtime types must be added independently without modifying the original type list. In that case, inheritance, type erasure, or another extensibility mechanism may be more appropriate.

A variant should not be selected solely because it can replace virtual dispatch. Performance depends on the alternatives, visitation patterns, memory layout, branch behavior, and compiler optimization. The design benefit is primarily that the allowed types are explicit and checked by the compiler. Profiling determines whether any particular dispatch strategy is faster.

Exercise

Create using SensorReading = std::variant<int, double, std::string>;. Store an integer RPM reading, a floating-point temperature reading, and a string status message in three separate variables. Write one visitor that prints each value. Then write an overloaded visitor that labels each value as RPM, Temperature, or Status. Finally, call index(), holds_alternative, and get_if on at least one variant.

Knowledge Check

1. How many alternatives can a std::variant hold at one time? 2. What does std::get_if return when the requested type is not active? 3. What is the main purpose of std::visit? 4. Why might std::monostate be placed first in a variant? 5. What does valueless_by_exception() mean?

Answers

1. Exactly one active alternative at a time. 2. A null pointer. 3. It dispatches a callable to the variant’s currently active alternative with compile-time type checking. 4. It provides a simple default-constructible neutral state. 5. An exceptional operation left the variant with no active alternative.

Elementary Conclusion

std::variant provides a disciplined way to represent “one of several known types.” std::get, std::get_if, and std::holds_alternative inspect the active value, while std::visit turns the variant into a type-safe dispatch mechanism. The essential design question is simple: when a value may legally take one of several fixed forms, a variant makes those forms part of the type system instead of hiding them behind unchecked tags, casts, or magic conventions.

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.

Leave a Reply