C++ Standard Library · Lesson 11

Vocabulary Types: pair, tuple, optional, variant

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 fluently
The one idea

Four 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.

01 pair — two values, bundled

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
Run on godbolt.org (-std=c++17+).
💡 Always unpack with structured bindings .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.

02 tuple — N values, bundled

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
⚠ Don't overuse tuples for "real" data A 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.

03 optional — a value, or nothing C++17

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
optional<int> = 42 has_value() == true · *r == 42 optional<int> = nullopt has_value() == false · empty
One type encodes "maybe a value." No magic sentinel to remember, no out-parameter — the absence is part of the type.
⚠ Don't deref an empty optional *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.

04 variant — one of several types C++17

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);
💡 variant vs a raw union A C 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.

05 Which vocabulary type?

You want to express…Type
Two related values togetherpair<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 varsstructured bindings: auto [a, b] = ...
🎯 Why this lesson pays off in reading A signature like 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++.

06 Check yourself

A function might or might not produce a result. The modern return type is:
What's the cleanest way to read a pair or tuple apart?
How is std::variant<int,string> safer than a C union?
You call .value() on an empty optional. What happens?

07 Flashcards

Q: What's a "vocabulary type"?
A: A small standard type that shows up across APIs as shared glue — pair, tuple, optional, variant — describing inputs/results so intent is visible.
Q: pair vs tuple?
A: pair bundles exactly two values (.first/.second); tuple bundles any number. Unpack both with structured bindings.
Q: What does std::optional<T> express, and how do you read it?
A: "A T, or nothing." Test with if (o)/has_value(), read with *o, or supply a fallback with value_or(x). Never deref when empty.
Q: What does std::variant<A,B> hold?
A: Exactly one value of one of the listed types, tracking which — a type-safe union. Query with holds_alternative/get, or dispatch with std::visit.
Q: When is a tuple the wrong choice?
A: For data that persists and has meaning — the unnamed fields get confusing. Use a small struct with named members instead; reserve tuples for transient multi-returns.
Q: How do structured bindings tie these together?
A: auto [a, b] = ... unpacks a pair, tuple, or struct into named variables — the same syntax used to iterate a map's entries.
👩‍🏫 I'm your teacher — ask me anything

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.