std::optional, std::variant, std::any — C++

std::optional<T> (C++17)

Represents a value that may or may not be present — a nullable value without pointers.

#include <optional>

std::optional<int> find_age(const std::string& name) {
    if (name == "Alice") return 30;
    return std::nullopt;
}

auto age = find_age("Alice");
if (age.has_value()) {
    std::cout << age.value() << "\n";  // 30
}
std::cout << age.value_or(0) << "\n"; // 30 or 0 if empty

// C++23 monadic operations
auto doubled = age.transform([](int v){ return v * 2; }); // optional<int>{60}

[!TIP] Use value_or(default) to safely extract a value from an optional without risking std::bad_optional_access.


std::variant<T1,T2,…> (C++17)

A type-safe union — holds exactly one value from a fixed set of types at any time.

#include <variant>

std::variant<int, double, std::string> v;
v = 42;                                    // holds int
v = 3.14;                                  // holds double
v = std::string("hello");                  // holds string

// Check which type
if (std::holds_alternative<std::string>(v)) {
    std::cout << std::get<std::string>(v) << "\n";
}

// std::visit with overloaded lambda
std::visit([](auto&& val) {
    std::cout << val << "\n";
}, v);

Overloaded visitor pattern:

// Helper to create a visitor from multiple lambdas
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

std::visit(overloaded{
    [](int i)               { std::cout << "int: "    << i << "\n"; },
    [](double d)            { std::cout << "double: " << d << "\n"; },
    [](const std::string& s){ std::cout << "string: " << s << "\n"; }
}, v);

[!NOTE] std::get<T>(v) throws std::bad_variant_access if the variant does not hold type T. Use std::get_if<T>(&v) for a non-throwing pointer-based access.


std::any (C++17)

Type-erased container — can hold any copyable value, type checked at runtime.

#include <any>

std::any a = 42;
a = std::string("hello");
a = 3.14;

// Access (throws std::bad_any_cast if wrong type)
try {
    double d = std::any_cast<double>(a);
    std::cout << d << "\n";
} catch (const std::bad_any_cast& e) {
    std::cerr << "wrong type\n";
}

// Safe access via pointer
if (auto* p = std::any_cast<double>(&a)) {
    std::cout << *p << "\n";
}

[!WARNING] std::any involves heap allocation and runtime type checks. Prefer std::variant when the set of types is known at compile time.


When to Use Each


optional Lifecycle

flowchart LR A[“std::nullopt (empty)”] –>|”assign value”| B[“has_value() == true”] B –>|”value()”| C[“Access T”] B –>|”reset()”| A A –>|”value_or(def)”| D[“Returns default”] B –>|”value_or(def)”| C

variant Visitor Pattern

flowchart TD A[“std::variant holds value”] –> B[“std::visit(visitor, v)”] B –> C{“Runtime type check”} C –>|”is int”| D[“Call visitor(int)”] C –>|”is double”| E[“Call visitor(double)”] C –>|”is string”| F[“Call visitor(string)”]

Decision Tree

flowchart TD A[“Need nullable or multi-type value?”] –> B{“Known fixed set of types?”} B –>|”Yes, one optional value”| C{“Can be absent?”} C –>|”Yes”| D[“Use std::optional”] C –>|”No”| E[“Use direct type”] B –>|”Yes, multiple types”| F[“Use std::variant”] B –>|”No, any type”| G[“Use std::any”]


Summary Table

TypeHeaderUse caseType safetyNotes
std::optional<T><optional>Nullable single valueCompile-timeC++17; monadic ops C++23
std::variant<Ts...><variant>One of N known typesCompile-timeC++17; use std::visit
std::any<any>Any copyable valueRuntimeC++17; slower, use sparingly