optional, variant & any
C++17 added three vocabulary types to the standard library that replace a family of error-prone patterns programmers had been reaching for by hand for decades: std::optional, std::variant, and std::any. All three live in headers of the same name and express, in the type system itself, something that used to be left to convention and comments.
std::optional — a value that might not exist
The old anti-patterns std::optional replaces
Before std::optional, a function that might not have a meaningful value to return typically resorted to one of two anti-patterns: returning a sentinel value (like -1 for an integer, which is ambiguous if -1 is ever a legitimate result), or returning a pointer that might be nullptr (which forces heap allocation or aliasing concerns just to express "absence"). Both approaches rely on the caller remembering, unenforced by the compiler, to check for the special case.
std::optional instead of a sentinel or nullable pointer
CPP
#include <iostream>
#include <optional>
#include <string>
#include <vector>
std::optional<int> findFirstNegative(const std::vector<int>& nums) {
for (int n : nums) {
if (n < 0) return n;
}
return std::nullopt; // clearly "no value", not a magic number
}
int main() {
std::vector<int> data = {4, 7, -3, 9};
if (auto result = findFirstNegative(data); result.has_value()) {
std::cout << "Found: " << result.value() << "\n";
} else {
std::cout << "No negative number found\n";
}
// Shorthand: an optional is contextually convertible to bool,
// and *opt dereferences it (undefined behavior if empty).
auto maybe = findFirstNegative(data);
if (maybe) {
std::cout << "Also found: " << *maybe << "\n";
}
return 0;
}std::variant — a type-safe union
std::variant<Types...> holds exactly one value at a time, whose type is one of the listed alternatives. Unlike a C-style union, a variant always knows which alternative is currently active and enforces that at runtime, so you cannot accidentally read the wrong member.
std::variant with std::get and std::visit
CPP
#include <iostream>
#include <string>
#include <variant>
int main() {
std::variant<int, std::string> value = 42;
// Access with std::get<T>() -- throws std::bad_variant_access
// if that alternative isn't the one currently held.
std::cout << std::get<int>(value) << "\n";
value = std::string("now a string");
// std::visit dispatches to the right handler for whichever
// alternative is active, without any manual type checks.
std::visit([](const auto& v) {
std::cout << "Visiting: " << v << "\n";
}, value);
return 0;
}std::any — a type-erased container
std::any can hold a value of literally any copyable type, decided entirely at runtime, with no fixed list of alternatives the way std::variant requires. Retrieving the value requires std::any_cast<T>, which throws std::bad_any_cast if you guess the wrong type.
std::any with any_cast
CPP
#include <any>
#include <iostream>
#include <string>
int main() {
std::any value = 10;
std::cout << std::any_cast<int>(value) << "\n";
value = std::string("switched type entirely");
std::cout << std::any_cast<std::string>(value) << "\n";
return 0;
}Type | Holds | Type safety |
|---|---|---|
std::optional<T> | One T, or nothing | Full compile-time type safety |
std::variant<A, B, ...> | One of a fixed, known set of types | Full compile-time type safety |
std::any | Any copyable type at all | Runtime-checked only via any_cast |
std::any is used far less often
Because std::any gives up compile-time type checking entirely, it is reached for much less frequently than optional or variant in idiomatic modern C++. Prefer std::variant whenever the set of possible types is known in advance — the compiler can then help you handle every case. Reserve std::any for genuinely open-ended situations, such as a generic property bag or plugin system where the type truly cannot be known until runtime.
std::optional<T> replaces sentinel values and nullable pointers for "maybe absent" results.
std::variant<Types...> is a type-safe union that always knows its active alternative.
std::any is a type-erased container for any copyable type, checked only at runtime.
Prefer optional/variant for compile-time safety; reach for any only when the type set is truly unbounded.