optional, variant and any (C++17)
C++17 added three vocabulary types that let you express common shapes of data precisely: std::optional<T> for a value that might be absent, std::variant<...> for a value that is one of several known types, and std::any for a single value of a type decided at runtime. By the end you'll know which one fits each problem — and why variant usually beats any .
Learn optional, variant and any (C++17) in our free C++ course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
Picture a small parcel locker. std::optional is a single box that is either empty or holds one item — you check before reaching in. std::variant is a box with a labelled slot list : it can hold a coin, a letter, or a key, but only one at a time, and the label always tells you which, so you never grab the wrong thing. std::any is a magic box that swallows anything at all — flexible, but you must remember exactly what you put in to get it back out, and guessing wrong throws it on the floor. Most of the time you know the shortlist of things you store, so the labelled box ( variant ) is the right tool.
1. std::optional — Maybe a Value
A function that might not have an answer used to return a sentinel ( -1 , an empty string) or take an out-parameter. std::optional<T> says it directly: the result is either engaged (holds a T ) or disengaged (empty, written std::nullopt ). Test it with has_value() or the implicit bool conversion, read it with * or -> , and supply a fallback with value_or() . Beware: calling .value() on an empty optional throws std::bad_optional_access .
Your turn. The division below can fail when the divisor is zero. Fill in the blank to print a fallback of -1 using value_or :
2. std::variant — One of Several Known Types
A std::variant<A, B, C> is a type-safe union : it holds a value of exactly one of its alternatives at any moment, and it always remembers which (its index() ). Unlike a raw union , it tracks the active type for you and refuses unsafe access. Ask holds_alternative<T>(v) which type is active, read it with get<T>(v) (which throws std::bad_variant_access on a mismatch) or probe safely with get_if<T>(&v) (which returns a pointer, or nullptr ).
When you need to act on whatever the variant holds, reach for std::visit . It applies a callable to the active alternative and, crucially, won't compile unless you handle every type — so adding an alternative later forces you to update the code. The common idiom bundles one lambda per type into an overloaded set:
A variant is default-constructed to its first alternative, which must therefore be default-constructible. When your real types aren't — or when you want an explicit "nothing chosen yet" state — list std::monostate first:
🧩 Reorder Challenge
These lines should make a variant, switch it to a string, and safely print it. Put them in the right order:
Open main (D), create the variant holding an int (B), reassign it to a string (A), then probe with get_if and print (C), then return 0; (E) and close the brace (F).
3. std::any — A Single Value of Any Type
std::any stores a single value whose type is chosen at runtime . It is the most flexible of the three and the least checked: you must recover the exact type with std::any_cast<T> , and a wrong guess throws std::bad_any_cast (the pointer form returns nullptr instead). Because it type-erases and may allocate, any is slower and offers no compile-time safety — so prefer variant whenever the candidate types are known, and keep any for genuinely open-ended storage like a heterogeneous property bag.
🔎 Deep Dive: which type fits which problem
optional answers "is there a value?" — one type, present or absent. variant answers "which of these types is it?" — a closed, compile-time-known set. any answers "some value, type decided later" — an open set, checked only at runtime. Reach down this list in order: most problems are an optional or a variant ; any is the rare last resort.
A handy combination is std::optional<std::variant<...>> : a result that might be absent, and when present is one of several types. And note that std::variant never heap-allocates for its alternatives, while std::any may — another reason to prefer variant on hot paths.
Pro Tips
- 💡 Prefer value_or over .value() when an empty optional is normal — it never throws.
- 💡 Use get_if in if -statements to probe a variant without risking bad_variant_access .
- 💡 Make std::visit your default for variants — it forces you to handle every alternative.
- 💡 Reach for variant before any : if you can list the types, you gain speed and compile-time safety.
🧠 Quick Recall
Predict the result before revealing the answer.
-1 — the optional is empty, so value_or returns the fallback.
It throws std::bad_variant_access — the active alternative is a string, not an int. Use get_if to probe safely.
3. Why list std::monostate first in a variant?
So the variant has a valid default-constructible first alternative and a clear "nothing chosen yet" state, even when the real type isn't default-constructible.
Common Errors (and the fix)
- Dereferencing an empty optional: *opt or .value() on a disengaged optional is undefined behaviour or a thrown bad_optional_access . Test with has_value() first, or use value_or() .
- Using get on the wrong alternative: get<T>(v) throws bad_variant_access when T isn't active. Use get_if<T>(&v) in an if to probe safely.
- Non-exhaustive visit : if a visitor can't handle every alternative, the code won't compile. Add a lambda (or a generic auto one) for each type.
- Wrong type in any_cast : casting to a type the any doesn't hold throws bad_any_cast . Match the stored type exactly, or use the pointer form and check for nullptr .
- Reaching for any too soon: if you can enumerate the types, a variant is safer and faster. Only use any when the set is truly open.
📋 Quick Reference
Task
Code
Maybe a value
optional<T> o;
Empty optional
o = nullopt;
Fallback
o.value_or(def)
Type-safe union
variant<A, B> v;
Which type?
holds_alternative<A>(v)
Safe access
get_if<A>(&v)
Dispatch
visit(vis, v)
Any type
any a; any_cast<T>(a)
Frequently Asked Questions
Mini-Challenge: A Tiny JSON Value
Model a JSON-like value with a variant (using monostate for null), then print it with std::visit and an overloaded set of lambdas — one per alternative.
🎉 Lesson Complete
- ✅ std::optional<T> models a maybe-value; use nullopt , has_value and value_or
- ✅ std::variant<...> is a type-safe union — probe with holds_alternative / get_if
- ✅ std::visit dispatches on the active alternative and forces exhaustive handling
- ✅ std::monostate gives a variant a valid default / empty state
- ✅ std::any stores any type via any_cast — prefer variant when types are known
- ✅ Next lesson: string_view and Non-Owning Views — borrow char data without copying
Practice quiz
What does std::optional<T> represent?
- A value that may or may not be present
- A pointer that is always valid
- A fixed-size array of T
- A thread-safe counter
Answer: A value that may or may not be present. std::optional<T> models a value that is either present or absent, replacing sentinel values or out-parameters for maybe-a-value.
Which call safely returns a fallback when an optional is empty?
- opt.value()
- *opt
- opt.value_or(fallback)
- opt.reset()
Answer: opt.value_or(fallback). value_or(fallback) returns the contained value if present, otherwise the supplied fallback — and never throws.
What happens if you call .value() on an empty std::optional?
- It is undefined behaviour
- It throws std::bad_optional_access
- It returns 0
- It returns nullptr
Answer: It throws std::bad_optional_access. Calling .value() on a disengaged optional throws std::bad_optional_access; use has_value() or value_or() to stay safe.
What is std::variant<int, std::string> best described as?
- A pointer to either type
- An array of int and string
- A container of many values
- A type-safe union holding exactly one of the listed types at a time
Answer: A type-safe union holding exactly one of the listed types at a time. std::variant is a type-safe tagged union: at any moment it holds a value of exactly one of its alternative types.
Which function tells you whether a variant currently holds a given alternative?
- std::holds_alternative<T>(v)
- v.contains<T>()
- std::any_cast<T>(v)
- v.type() == T
Answer: std::holds_alternative<T>(v). std::holds_alternative<T>(v) returns true when the variant currently holds an alternative of type T.
What does std::get_if<T>(&v) return when the variant does NOT hold T?
- It throws std::bad_variant_access
- A null pointer
- An empty optional
- A default-constructed T
Answer: A null pointer. get_if takes a pointer and returns a pointer to the value if the variant holds T, or nullptr otherwise — it never throws.
What is std::visit used for?
- Visiting every node of a tree
- Copying a variant
- Iterating a vector
- Applying a callable to whichever alternative a variant currently holds
Answer: Applying a callable to whichever alternative a variant currently holds. std::visit dispatches a visitor (often an overloaded callable) to the currently active alternative of a variant.
What is std::monostate typically used for?
- To cast between alternatives
- To make a variant thread-safe
- To give a variant a valid 'empty' or default first alternative
- To store any type
Answer: To give a variant a valid 'empty' or default first alternative. std::monostate is an empty placeholder type, commonly listed first so a variant has a default-constructible 'no value yet' state.
How do you retrieve the stored value from a std::any holding an int?
- a.value()
- any_cast<int>(a)
- a.get<int>()
- *a
Answer: any_cast<int>(a). std::any_cast<int>(a) extracts the int; a bad cast throws std::bad_any_cast (or returns nullptr for the pointer form).
When should you generally avoid std::any?
- Whenever the set of possible types is known at compile time
- When you need a maybe-value
- When performance does not matter
- When you only store ints
Answer: Whenever the set of possible types is known at compile time. If the candidate types are known, std::variant is safer and faster; std::any erases the type and should be reserved for truly open-ended storage.
Continue this course
- Previous: Numerical Computing with Eigen
- Next: string_view and Non-Owning Views