These vocabulary types replace sentinel values, null pointers and many exceptions with types the compiler checks.
`std::optional<T>` (C++17) holds either a T or nothing. It's the clear return type for "might not have a result": std::optional<int> parse_int(std::string_view). Test it with if (opt) or has_value(), read it with *opt (unchecked) or opt.value() (throws std::bad_optional_access), and supply a fallback with value_or(x). The value is stored inline, so there's no heap allocation. C++23 adds monadic chaining: and_then, transform and or_else.
`std::variant<A, B, C>` (C++17) holds exactly one of several types, a type-safe tagged union, also stored inline. It is a closed alternative to inheritance: no virtual functions, no heap, and the set of types is fixed.
std::holds_alternative<B>(v)tests the active type,std::get<B>(v)reads it (throws if wrong),std::get_if<B>(&v)returns a pointer or null.std::visit(visitor, v)calls the visitor with the active alternative. With the overloaded helper (a struct inheriting from several lambdas) you write one lambda per type, and forgetting a type is a compile error.
`std::expected<T, E>` (C++23, <expected>) holds either a value or an error of type E. It reports failure without exceptions while keeping the reason:
std::expected<Config, std::string> load(std::string_view path);
auto cfg = load("app.toml");
if (!cfg) log(cfg.error());Return std::unexpected(err) to signal failure. It has the same monadic operations as optional.
Which to use: exceptions for rare failures far from where they're handled; optional when absence needs no explanation; expected when callers handle errors right away and need the reason; variant for states and messages with a fixed set of shapes.
Example
#include <charconv>
#include <iostream>
#include <optional>
#include <string_view>
#include <variant>
std::optional<int> parse_int(std::string_view s) {
int v{};
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
if (ec != std::errc{} || ptr != s.data() + s.size()) return std::nullopt;
return v;
}
struct Circle { double r; };
struct Rect { double w, h; };
using Shape = std::variant<Circle, Rect>; // exactly one of these, no heap
template <class... Fs> struct overloaded : Fs... { using Fs::operator()...; };
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) { return 3.14159 * c.r * c.r; },
[](const Rect& r) { return r.w * r.h; },
}, s); // missing a case = compile error
}
int main() {
std::cout << parse_int("42").value_or(-1) << ' ' << parse_int("4x").value_or(-1) << '\n'; // 42 -1
if (auto n = parse_int("7")) std::cout << *n * 2 << '\n'; // 14
Shape s = Rect{2, 3};
std::cout << area(s) << ' ' << std::holds_alternative<Rect>(s) << '\n'; // 6 1
}Remember
optional<T>: a value or nothing;value_orgives a fallbackvariant+std::visit: a closed set of types, checked exhaustively- C++23
expected<T, E>: a value or an error, without exceptions
Watch out for
- Calling
*optwithout checking — dereferencing an empty optional is undefined behaviour.