Below is a practical, interview-ready mental model for
std::optional, std::variant, and std::any — focused on avoiding null pointers and expressing absence safely.
No fluff, no emojis, straight engineering trade-offs.
Classic C++ problems:
❌ Returning nullptr
❌ Returning invalid sentinel values
❌ Output parameters
❌ Boolean + output value patterns
❌ Implicit assumptions (“this pointer might be null”)
Modern C++ replaces these with type-level guarantees.
A value may or may not exist
A pointer without ownership, allocation, or lifetime issues
- Function may fail to produce a value
- Cache lookup
- Optional configuration
- Parsing results
- Lazy initialization
std::optional<int> parseInt(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt;
}
}Usage:
if (auto v = parseInt("42")) {
std::cout << *v;
}❌ int* → who owns it? is it null?
✅ std::optional<int> → explicit absence
- No heap allocation
- Value stored inline
- Checked access
- Forces caller to acknowledge absence
- When absence is impossible
- When you need multiple alternative types
- When lifetime must be shared
Exactly one of several known types
A type-safe tagged union
- State machines
- Message passing
- AST nodes
- API results with multiple valid forms
- Replacing base-class polymorphism when behavior is simple
using Result = std::variant<int, std::string>;
Result r = 42;Usage:
std::visit([](auto&& v) {
std::cout << v;
}, r);using Result = std::variant<std::monostate, int>;But:
❌ worse than std::optional<int>
✅ use optional unless absence is one of many states
❌ virtual dispatch ❌ heap allocation ❌ fragile base classes
✅ compile-time exhaustiveness ✅ no RTTI ✅ no allocation
- One active alternative at a time
- Known set of types
- Compile-time checked handling
- Open-ended types
- Plugin systems
- When type list is not stable
Some value of unknown type
Type-erased storage with runtime type checking
- Framework internals
- Plugin APIs
- Heterogeneous containers
- Metadata
- Configuration blobs
std::any a = 42;
if (a.type() == typeid(int)) {
int v = std::any_cast<int>(a);
}❌ No compile-time guarantees ❌ Runtime errors if misused ❌ Easy to abuse ❌ Slower
std::any a; // emptyBut:
❌ unclear intent
❌ no documentation in the type
❌ worse than optional
If you think you need
std::any, question the design first
| Feature | optional<T> |
variant<Ts...> |
any |
|---|---|---|---|
| Expresses absence | YES | Indirectly | YES (weak) |
| Type safety | Strong | Strong | Weak |
| Compile-time checks | YES | YES | NO |
| Heap allocation | NO | NO | Maybe |
| Intent clarity | Excellent | Excellent | Poor |
| Runtime overhead | Minimal | Minimal | Higher |
| Best for APIs | YES | YES | Rarely |
Foo* findFoo();std::optional<Foo> findFoo();or if ownership matters:
std::unique_ptr<Foo> findFoo();If something must exist, use:
Foo&If it might not exist, use:
std::optional<std::reference_wrapper<Foo>>-
Can it be absent? →
std::optional<T> -
Is it one of a fixed set of types? →
std::variant<Ts...> -
Is the type unknown or open-ended? →
std::any(last resort)
You can safely say:
- “
std::optionalencodes absence in the type system.” - “
std::variantgives compile-time checked alternatives without polymorphism.” - “
std::anytrades safety for flexibility and should be isolated.” - “Null pointers express failure poorly; modern C++ prefers semantic types.”