std::optional, std::variant & std::any
Scenario
You want to return a user if found, or 'nothing' (but returning `nullptr` is unsafe). Alternatively, you're parsing a JSON config where a node could be an integer, a float, or a string.
How do you safely represent "a value that might not exist" without resorting to raw pointers?
Why it exists
C++ traditionally lacked safe vocabulary types for missing values or heterogeneous data. Developers relied on dangerous C-style memory tricks (like returning NULL pointers, using magic error numbers like -1, or raw `union`s). C++17 introduced these three types to provide memory-safe, standardized ways to represent optional, alternative, or dynamic values.
Mental model
`std::optional` is a cardboard box that is either completely empty, or contains exactly one item. `std::variant` is a multi-tool that can be exactly one of several specific tools at any given time. `std::any` is a magical bag that can hold literally anything, but you have to guess what's inside to take it out.
Modern C++ (C++17) introduced these three vocabulary types to safely handle uncertainty and dynamic typing, completely replacing older, unsafe C-style paradigms like returning magic numbers (-1), using raw `union`s, or casting `void*` pointers.
Explanation
**1. std::optional<T>**
Represents a value that may or may not be present. It is the modern replacement for returning `nullptr` or magic error codes. You check if it has a value using `if (opt.has_value())` or simply `if (opt)`. You extract the value using `opt.value()` (which throws an exception if empty) or `*opt`.
**2. std::variant<T1, T2...>**
It is a type-safe union. It holds exactly one of the listed types. Unlike a C-style `union`, it knows exactly which type is currently active. You access it using `std::get<T>(var)` (which throws if you ask for the wrong type) or by using the visitor pattern with `std::visit` (a safe way to run a function that matches whatever type is currently active).
**3. std::any**
It can hold a single value of *any* type. It is the C++ equivalent of Python's dynamic typing or C's `void*`. You extract data using `std::any_cast<T>(my_any)`. It can involve dynamic memory allocation (though implementations use small-object optimization for small types) and type-checking overhead, so it should be used sparingly.
**Real-world Engineering:**
`std::optional` is heavily used in API design to signify that a query might not yield a result. `std::variant` is the cornerstone of writing Abstract Syntax Trees (ASTs) in compilers, or handling JSON objects where a node might be a String, an Array, or an Integer. Avoid `std::any` unless you are building a highly dynamic system like an event bus where payloads are completely unknown.
Code examples
Using std::optional
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> getNickname(bool isFriend) {
if (isFriend) return "Buddy";
return std::nullopt; // Represents 'empty'
}
int main() {
auto nickname = getNickname(true);
// Safely check if it exists
if (nickname.has_value()) {
std::cout << nickname.value() << '\n';
}
// Provide a fallback if empty
auto badNick = getNickname(false);
std::cout << badNick.value_or("Stranger") << '\n';
return 0;
}
`value_or` is an incredibly useful method that unwraps the value or returns a default fallback, completely eliminating branching `if` statements.
Key points
- Optional replaces nullptr: Never return pointers or magic values just to signal 'not found'. Use `std::optional`.
- Variant is a safe union: Use `std::variant` when an object must be exactly one of a specific, known set of types.
Common mistakes
- Dereferencing an empty optional: If you use the pointer syntax `*opt` on an empty `std::optional`, it results in Undefined Behavior (usually a crash). If you use `opt.value()`, it safely throws a `std::bad_optional_access` exception. Prefer `.value()` unless performance is ultra-critical.
Recall questions
- What is the difference between `std::variant` and a standard C-style `union`?
- How do you represent an empty state for a `std::optional` return value?
- Why is `std::any` generally discouraged compared to `std::variant`?
Questions & answers
A function returns a `std::optional<int>`. A developer extracts the value using `.value()`. During execution, the program abruptly terminates with an unhandled exception. What exception was thrown and why?
It threw `std::bad_optional_access` because the optional was empty (`std::nullopt`) when `.value()` was called. They should have checked `.has_value()` first, or used `.value_or(default_val)`.
Approach: Understand the error-handling semantics of C++17 vocabulary types.
You are parsing a config file where a setting's value could be an Integer, a Float, or a String. Which modern C++ type should you use to store the value?
`std::variant<int, float, std::string>`. This perfectly captures the finite set of possible types in a type-safe manner without the overhead of `std::any`.
Approach: Map domain constraints to the correct vocabulary wrapper.
Continue learning
Previous: Templates — function & class templates