The small "glue" types that turn up all over modern signatures: bundle a few values, represent "a value or nothing," or "one of several types" — safely and readably.
🎯 Read & write modern API signatures fluentlyFour little types do an outsized amount of work in modern C++ APIs. pair/tuple bundle a fixed set of values; optional says "a value, or nothing"; variant says "one of these types." They're called vocabulary types because they're the shared words APIs use to describe their inputs and results.
A std::pair<A,B> glues two values together as .first and .second. You've already met it: a map entry is a pair<const K, V>, and insert returns a pair<iterator, bool>.
#include <utility> std::pair<std::string, int> e = {"Ada", 42}; e.first; // "Ada" e.second; // 42 // structured bindings (C++17) give the parts real names: auto [name, score] = e; // name = "Ada", score = 42
-std=c++17+)..first/.second are nameless and easy to mix up. auto [name, score] = e; reads far better — and it's the same syntax you use to iterate a map. Prefer it everywhere.Source: cppreference — std::pair.
std::tuple is pair generalized to any number of elements. Its best everyday use is returning several values from a function without inventing a struct.
#include <tuple> std::tuple<int, int, std::string> stats() { return {200, 12, "ok"}; // code, count, message } auto [code, count, msg] = stats(); // unpack all three at once std::get<0>(some_tuple); // access by index when needed
tuple<int,int,string> has no field names — three months later, which int was the count? For anything with meaning that lives beyond one call, a small struct with named members is clearer. Use tuples for transient, internal multi-returns.Source: cppreference — std::tuple.
How do you say "this might not return a result"? Old code abused sentinels (-1, empty string, nullptr) or output parameters. std::optional<T> says it directly: it either holds a T or is empty — type-safe and self-documenting.
#include <optional> std::optional<int> parse(std::string_view s) { if (looks_like_number(s)) return to_int(s); return std::nullopt; // "no value" } auto r = parse("42"); if (r) // or r.has_value() use(*r); // deref to get the value int port = parse(cfg).value_or(8080); // value, or a fallback if empty
*r / r.value() on an empty optional is a bug (value() throws; * is undefined behavior). Always test first, or use value_or(default).Source: cppreference — std::optional.
std::variant<A, B, C> holds exactly one value, of one of the listed types, and remembers which — a type-safe union. Great for "a result that is either a number or an error message," or a small set of states.
#include <variant> std::variant<int, std::string> v = 42; // currently holds an int v = "oops"; // now holds a string if (std::holds_alternative<std::string>(v)) std::cout << std::get<std::string>(v); // extract the active type // std::visit applies a handler to whichever type is active: std::visit([](auto&& x){ std::cout << x; }, v);
union doesn't track which member is valid — read the wrong one and it's undefined behavior. variant remembers the active type and checks access, so it's safe. (std::any is the looser cousin: it holds any single type, not a fixed set — reach for it rarely.)Source: cppreference — std::variant.
| You want to express… | Type |
|---|---|
| Two related values together | pair<A,B> |
| Return several values at once (transient) | tuple<...> (or a named struct if it persists) |
| "A result, or nothing" | optional<T> |
| "Exactly one of these few types" | variant<A,B,...> |
| Unpack any of the above into named vars | structured bindings: auto [a, b] = ... |
optional<User> find_user(...) tells you instantly: "might not find one — handle the empty case." These types make intent visible. Recognizing them is half of reading modern C++.pair or tuple apart?std::variant<int,string> safer than a C union?.value() on an empty optional. What happens?pair, tuple, optional, variant — describing inputs/results so intent is visible.pair vs tuple?pair bundles exactly two values (.first/.second); tuple bundles any number. Unpack both with structured bindings.std::optional<T> express, and how do you read it?T, or nothing." Test with if (o)/has_value(), read with *o, or supply a fallback with value_or(x). Never deref when empty.std::variant<A,B> hold?holds_alternative/get, or dispatch with std::visit.tuple the wrong choice?struct with named members instead; reserve tuples for transient multi-returns.auto [a, b] = ... unpacks a pair, tuple, or struct into named variables — the same syntax used to iterate a map's entries.Try: "refactor this function that returns -1 on failure to use optional", or "model a parse result as a variant<Value, Error>". Next: getting data in and out — I/O streams.