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 anoptionalwithout riskingstd::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)throwsstd::bad_variant_accessif the variant does not hold typeT. Usestd::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::anyinvolves heap allocation and runtime type checks. Preferstd::variantwhen the set of types is known at compile time.
When to Use Each
optional<T>: when a value might be absent (nullable return values, optional parameters)variant<T1,T2>: when a value must be one of a known set of types (tagged union, state machines)any: when the type is truly unknown at compile time (plugin systems, scripting bridges)
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
| Type | Header | Use case | Type safety | Notes |
|---|---|---|---|---|
std::optional<T> | <optional> | Nullable single value | Compile-time | C++17; monadic ops C++23 |
std::variant<Ts...> | <variant> | One of N known types | Compile-time | C++17; use std::visit |
std::any | <any> | Any copyable value | Runtime | C++17; slower, use sparingly |