optional, variant and expected

Three standard "vocabulary types" make common situations explicit in the type system: std::optional<T> for "a value or nothing", std::variant<A, B…> for "one of several types", and C++23's std::expected<T, E> for "a result or an error". They replace sentinel values, null pointers and ad-hoc unions.

std::optional

When a function may have no answer — a search that finds nothing, a parse that fails — return an optional instead of a magic value like -1:

C++
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<std::size_t> find_index(const std::vector<std::string>& v, const std::string& wanted) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (v[i] == wanted) return i;
    }
    return std::nullopt;                      // no value
}

int main() {
    std::vector<std::string> names{"Ana", "Ben", "Chloe"};
    if (auto i = find_index(names, "Ben")) {          // true if it holds a value
        std::cout << "Ben at index " << *i << '\n';
    }
    auto z = find_index(names, "Zoe");
    std::cout << std::boolalpha << z.has_value() << '\n';
    std::cout << "Zoe index or 99: " << z.value_or(99) << '\n';
}
Output
Ben at index 1
false
Zoe index or 99: 99
OperationMeaning
if (opt) / opt.has_value()Is there a value?
*opt, opt->memberThe value (undefined behaviour if empty)
opt.value()The value, or throws std::bad_optional_access
opt.value_or(fallback)The value, or the fallback
opt = std::nullopt; / opt.reset()Make it empty

optional also suits optional members ("middle name") and lazily computed values.

std::variant

A variant holds exactly one value from a fixed list of types, and knows which. It's a type-safe union:

C++
#include <iostream>
#include <string>
#include <variant>
#include <vector>

using Setting = std::variant<int, double, bool, std::string>;

int main() {
    std::vector<std::pair<std::string, Setting>> config{
        {"port", 8080}, {"ratio", 0.75}, {"debug", true}, {"host", std::string{"example.org"}}};

    for (const auto& [key, value] : config) {
        std::cout << key << " = ";
        std::visit([](const auto& v) { std::cout << v; }, value);   // call with whatever it holds
        std::cout << " (type index " << value.index() << ")\n";
    }

    Setting s = 42;
    if (std::holds_alternative<int>(s)) std::cout << "an int: " << std::get<int>(s) << '\n';
    s = std::string{"now a string"};
    std::cout << std::get<std::string>(s) << '\n';
}
Output
port = 8080 (type index 0)
ratio = 0.75 (type index 1)
debug = 1 (type index 2)
host = example.org (type index 3)
an int: 42
now a string

std::visit calls a function with the current value; a generic lambda handles every alternative. std::get<T> throws std::bad_variant_access if the variant holds a different type; std::get_if returns a pointer or nullptr instead.

Handling each type differently

The "overloaded" helper builds one visitor from several lambdas — a common idiom:

C++
#include <cmath>
#include <iostream>
#include <variant>
#include <vector>

struct Circle { double r; };
struct Rect { double w, h; };
struct Triangle { double a, b, c; };
using Shape = std::variant<Circle, Rect, Triangle>;

template <typename... Fs>
struct overloaded : Fs... { using Fs::operator()...; };

double area(const Shape& s) {
    return std::visit(overloaded{
        [](const Circle& c) { return 3.14159265 * c.r * c.r; },
        [](const Rect& r) { return r.w * r.h; },
        [](const Triangle& t) {
            double p = (t.a + t.b + t.c) / 2;
            return std::sqrt(p * (p - t.a) * (p - t.b) * (p - t.c));
        },
    }, s);
}

int main() {
    std::vector<Shape> shapes{Circle{1}, Rect{2, 3}, Triangle{3, 4, 5}};
    for (const auto& s : shapes) std::cout << area(s) << '\n';
}
Output
3.14159
6
6

This is an alternative to virtual functions (lesson 37): no heap allocation and no inheritance, and the compiler checks that every alternative is handled. It suits closed sets of types that rarely change.

std::expected (C++23)

std::expected<T, E> holds either a value of type T or an error of type E. It brings error-code-style handling with the convenience of optional — the error says why there's no value. It needs a C++23 compiler (GCC 12+, Clang 16+ with libc++), and this example was compiled with -std=c++23:

C++
#include <expected>
#include <iostream>
#include <string>

enum class ParseError { empty, not_a_number, out_of_range };

std::expected<int, ParseError> parse_percent(const std::string& s) {
    if (s.empty()) return std::unexpected(ParseError::empty);
    int value = 0;
    for (char c : s) {
        if (c < '0' || c > '9') return std::unexpected(ParseError::not_a_number);
        value = value * 10 + (c - '0');
        if (value > 100) return std::unexpected(ParseError::out_of_range);
    }
    return value;
}

const char* describe(ParseError e) {
    switch (e) {
        case ParseError::empty:        return "empty input";
        case ParseError::not_a_number: return "not a number";
        case ParseError::out_of_range: return "more than 100";
    }
    return "?";
}

int main() {
    for (std::string s : {"75", "", "7x", "250"}) {
        auto result = parse_percent(s);
        if (result) std::cout << '"' << s << "\" -> " << *result << "%\n";
        else std::cout << '"' << s << "\" -> error: " << describe(result.error()) << '\n';
    }
}
Output
"75" -> 75%
"" -> error: empty input
"7x" -> error: not a number
"250" -> error: more than 100

Like optional, it supports value_or, and C++23 adds chaining with and_then, transform and or_else.

Which to use?

SituationUse
"No result" is normal and needs no explanationstd::optional<T>
A value is one of a fixed set of typesstd::variant<…>
Failure is expected and the caller needs the reasonstd::expected<T, E>
Failure is exceptional and should propagate upAn exception (lesson 42)

Try it yourself

Write a function that looks up a product's price and returns an optional; total a shopping list, skipping unknown items.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
no price for caviar
total: 6.65