C++ Vocabulary Types
- Description: A note on C++ "vocabulary" types —
std::pair,std::tuple, structured bindings,std::optional,std::variant,std::any, andstd::expected(C++23) - My Notion Note ID: K2A-B1-14
- Created: 2019-05-20
- Updated: 2026-02-28
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Why Vocabulary Types?
- 2.
std::pairandstd::tuple - 3. Structured Bindings (C++17)
- 4.
std::optional<T> - 5.
std::variant<T...> - 6.
std::any - 7.
std::expected<T, E>(C++23) - 8. Decision Guide
1. Why Vocabulary Types?
- "Vocabulary types" = stdlib types for common patterns — maybe a value, one of several types, value or error, tuple of values.
- API uses them → everyone agrees on meaning. Avoid ad-hoc conventions:
-1for failure, output params, magic sentinels.
2. std::pair and std::tuple
std::pair<A, B>+std::tuple<Ts...>— heterogeneous fixed-size containers.
#include <utility> // pair
#include <tuple> // tuple
std::pair<std::string, int> kv{"Alice", 30};
kv.first; // "Alice"
kv.second; // 30
std::tuple<int, std::string, double> t{1, "two", 3.0};
std::get<0>(t); // 1
std::get<1>(t); // "two"
std::get<std::string>(t); // "two" (by type, C++14)
auto t2 = std::make_tuple(1, "two", 3.0); // type-deduced
auto p = std::make_pair("a", 1);
std::tuple_size_v<decltype(t)>; // 3
- Pairs = values inside
std::map. - Use pair/tuple only when "the things together" has no meaningful name; otherwise → named struct (
Person{name, age}reads better thanpair<string, int>).
3. Structured Bindings (C++17)
- Unpack tuples, pairs, arrays, aggregate structs into named locals.
std::pair<int, std::string> kv{1, "one"};
auto [n, s] = kv; // n = 1, s = "one"
std::map<std::string, int> ages = {/* ... */};
for (const auto& [name, age] : ages) {
std::cout << name << " is " << age;
}
struct Point { int x, y, z; };
Point p{1, 2, 3};
auto [x, y, z] = p; // works on aggregates
// const auto& and auto& are also supported
const auto& [name, age] = *ages.begin(); // no copy
auto& [a, b] = kv; // mutable references — write through
- Idiomatic way to iterate maps + consume multi-return functions in modern C++.
4. std::optional<T>
std::optional<T>— "maybe aT". Either holdsTor empty (std::nullopt).
#include <optional>
std::optional<int> find_id(const std::string& name) {
if (name == "Alice") return 42;
return std::nullopt; // or just `return {};`
}
auto id = find_id("Alice");
if (id) { // contextual bool: true when set
std::cout << *id; // dereference like a pointer
std::cout << id.value(); // throws std::bad_optional_access if empty
}
int n = id.value_or(0); // 42 or default
id.reset(); // clear
// Monadic operations (C++23)
auto result = id
.transform([](int x) { return x * 2; }) // map: optional<int> -> optional<int>
.or_else([] { return std::optional{0}; }) // alternative if empty
.and_then([](int x) -> std::optional<int> { // chain that may return nullopt
return x > 0 ? std::optional{x} : std::nullopt;
});
Use optional<T> for:
- Return values that may be absent — instead of
-1,nullptr,bool + output param. - Optional members of a struct.
- Lazy init — store empty, populate on first use.
- Don't use for "value or error" — use
expected<T, E>.optionalonly says "did it work", not why.
5. std::variant<T...>
std::variant<Ts...>— type-safe union. Holds exactly one value from a fixed alternative list, knows which, prevents wrong-type access.
#include <variant>
std::variant<int, std::string, double> v; // initialized to int{}
v = "hello"; // now holds string
v = 3.14; // now holds double
v.index(); // 2 (third alternative)
std::holds_alternative<double>(v); // true
std::get<double>(v); // 3.14 (throws bad_variant_access if wrong)
std::get<2>(v); // 3.14 (by index)
// std::visit applies a callable to whichever alternative is held:
std::visit([](auto&& val) {
std::cout << val; // works because << is defined for all 3
}, v);
// Type-specific handling via a visitor struct:
struct Visitor {
void operator()(int x) { std::cout << "int: " << x; }
void operator()(double x) { std::cout << "double: " << x; }
void operator()(const std::string& s) { std::cout << "str: " << s; }
};
std::visit(Visitor{}, v);
Use variant for:
- Sum types / tagged unions — "this is one of A, B, or C."
- State machines — each state = different type.
- Heterogeneous error returns —
variant<Success, NetworkError, ParseError>.
- Type-safe: can't read wrong type without
get(throws on mismatch) orholds_alternative.
6. std::any
std::any(C++17) — holds any type. Recovered viaany_cast<T>. Type-erased.- Impls encouraged to apply small-object optimization for small, nothrow-move types (
int,double, small structs typically avoid heap). Larger/throwing-move types → heap.
#include <any>
std::any a = 42;
a = std::string("hello");
a = 3.14;
double d = std::any_cast<double>(a); // throws bad_any_cast on type mismatch
double* p = std::any_cast<double>(&a); // pointer overload: returns nullptr on mismatch
- Rarely the right answer. Prefer
variant(compile-time-known alternatives) or virtual interface (need polymorphism). anymainly for frameworks that genuinely don't know value type — config systems, plugin APIs, scripting bridges.
7. std::expected<T, E> (C++23)
std::expected<T, E>— "aTon success, or anEon error". Richer thanoptionalfor fallible ops.- Modern alternative to error codes + exceptions.
#include <expected>
enum class ParseError { Empty, InvalidChar, Overflow };
std::expected<int, ParseError> parse(std::string_view s) {
if (s.empty()) return std::unexpected{ParseError::Empty};
int n = 0;
for (char c : s) {
if (!std::isdigit(c)) return std::unexpected{ParseError::InvalidChar};
n = n * 10 + (c - '0');
}
return n;
}
auto r = parse("42");
if (r) {
std::cout << *r; // dereference like optional
}
if (!r) {
auto err = r.error(); // access the error
}
// Monadic chaining:
auto doubled = parse("21")
.transform([](int x) { return x * 2; })
.or_else([](ParseError) { return std::expected<int, ParseError>{0}; });
Use expected when:
- Function might fail in a small number of well-defined ways.
- Callers should handle inline (not propagate via exceptions).
- You need error reason, not just "failed."
- See also: C++ Error Handling § 6.
8. Decision Guide
| Situation | Use |
|---|---|
| Return value or "missing" | std::optional<T> |
| Return value or specific error | std::expected<T, E> (C++23) |
| Multiple possible types from a fixed list | std::variant<T...> |
| Two values of related kinds | struct (or std::pair if names don't matter) |
| Several values, possibly unrelated | std::tuple (or a struct) |
| Type genuinely unknown at compile time | std::any (rare) |
| "Failed but with no info" | bool, or optional |