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.

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:

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.

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.

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.

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.

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.