std::optional, std::variant & std::any

RoadmapsC++

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

Common mistakes

Recall questions

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

Return to C++ Roadmap