Skip to content
C++
C++17Intermediate

std::optional, std::variant, std::any

C++17 added three types that handle a common problem in different ways: how to represent a value that might not exist, might be one of several types, or might be any type at all. These are called vocabulary types — standard library components so fundamental that code should express itself in terms of them. Each one replaces a dangerous or error-prone pattern: sentinel values, tagged unions, and void pointers.

std::optional<T> — a nullable value

std::optional<T> either contains a value of type T, or it contains nothing (std::nullopt). It replaces sentinel values like -1, nullptr, or ""that are implicitly "no value" by convention. An optional makes the maybe-nothing case explicit in the type.

#include <optional>

// Before optional — returning -1 as sentinel (fragile, surprising):
int find_index(const std::vector<int>& v, int target) {
    for (int i = 0; i < v.size(); ++i)
        if (v[i] == target) return i;
    return -1;   // what if a real index is -1? What if T changes?
}

// With optional — the absence of a value is in the type:
std::optional<int> find_index(const std::vector<int>& v, int target) {
    for (std::size_t i = 0; i < v.size(); ++i)
        if (v[i] == target) return i;   // implicitly wraps in optional
    return std::nullopt;                 // explicitly no value
}

// Using the result:
auto idx = find_index(v, 42);

if (idx.has_value()) {
    std::cout << "Found at " << idx.value();
}

// Or with operator bool:
if (idx) {
    std::cout << "Found at " << *idx;  // operator* accesses the value
}

// Or with value_or — provide a default:
std::cout << idx.value_or(-1);  // prints -1 if not found

// Building optionals:
std::optional<std::string> name = "Alice";   // has value
std::optional<std::string> empty;            // no value (same as std::nullopt)
auto constructed = std::make_optional(42);   // has_value() == true

Calling value() on an empty optional throws std::bad_optional_access. Dereferencing with * or -> on an empty optional is undefined behavior — always check first.

std::variant<Ts...> — a type-safe union

std::variant holds exactly one value from a fixed set of types. It is a type-safe replacement for C union: you can never accidentally read the wrong type, because the variant tracks which type is currently active. This is useful for representing a value that could legitimately be one of several distinct types.

#include <variant>

// A value that is either an int, a double, or a string:
std::variant<int, double, std::string> v;

v = 42;         // now holds an int
v = 3.14;       // now holds a double — int is destroyed
v = "hello";    // now holds a std::string — double is destroyed

// Checking which type is active:
std::holds_alternative<std::string>(v);  // true

// Accessing the active value:
std::get<std::string>(v);               // "hello"
std::get<int>(v);                       // throws std::bad_variant_access!

// Safe access with get_if (returns pointer or nullptr):
if (auto* s = std::get_if<std::string>(&v)) {
    std::cout << "string: " << *s;
}

// std::visit — apply a callable to whichever type is active:
std::visit([](auto&& val) {
    std::cout << val;  // works for int, double, and string
}, v);

// Visitor with overloaded lambdas (C++17 trick):
auto visitor = [](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>)
        std::cout << "int: " << arg;
    else if constexpr (std::is_same_v<T, double>)
        std::cout << "double: " << arg;
    else
        std::cout << "string: " << arg;
};
std::visit(visitor, v);

A common use of variant is error handling:std::variant<Result, Error> is a simpler version of std::expected (C++23) and a type-safe replacement for returning error codes alongside a value.

std::any — a type-erased value

std::any holds a value of any copyable type. Unlike variant, the set of types is not fixed at compile time — you can store anything. The trade-off is that type information is checked at runtime with any_cast, which throws if the requested type is wrong. Think of it as a type-safe void*.

#include <any>

std::any a = 42;          // holds an int
a = std::string("hi");    // now holds a string — int is destroyed
a = 3.14;                 // now holds a double

// Check type:
a.type() == typeid(double);   // true
a.has_value();                // true

// Casting — throws std::bad_any_cast on type mismatch:
std::any_cast<double>(a);     // 3.14
std::any_cast<int>(a);        // throws! it's a double

// Safe cast via pointer (returns nullptr on mismatch):
if (auto* d = std::any_cast<double>(&a)) {
    std::cout << *d;
}

// Constructing in-place:
auto a2 = std::make_any<std::vector<int>>(3, 0);  // vector{0,0,0}

// Resetting:
a.reset();
a.has_value();   // false

std::any performs a heap allocation for values larger than a small buffer (the exact threshold is implementation-defined). It is the most flexible but least efficient and least safe of the three vocabulary types. Prefer optional or variant when the type set is known.

Choosing the right type

TypeUse whenCost
std::optional<T>A value may or may not be presentZero overhead — stored inline
std::variant<Ts...>Value is one of N known typesSize of largest type + tag byte
std::anyType is unknown at compile timeHeap alloc for large values

Key rules to remember

Accessing an empty optional with value() throws; with operator* is UB

Always check has_value() or operator bool before dereferencing. Use value_or() to provide a fallback without checking.

std::get<T>(variant) throws bad_variant_access if T is not the active type

Use std::get_if<T>(&variant) to get a pointer that is nullptr on mismatch, or std::holds_alternative<T>(variant) to check first.

Prefer std::visit over manual type-checking for variant

visit forces you to handle every possible type at compile time. Manual get/holds_alternative chains can miss cases silently.

std::any is a last resort — prefer optional or variant when types are known

any loses compile-time type information and may heap-allocate. If you find yourself reaching for any, consider whether variant with a closed set of types is more appropriate.

Sign in to track progress