std::pair<T1, T2> glues two values of possibly different types into one object with fields first and second. std::tuple<Ts...> generalises the idea to any number of fields, accessed by index or by type. Both show up across the standard library (especially in map iteration and algorithms that need to return more than one value), and both are useful when you want a quick ad-hoc record without writing a full struct. This chapter covers what they are, how to construct and unpack them, when one is the right answer, and when a named struct beats both.
Functions sometimes need to return two things together. Looking up a product by name returns both the price and whether it was found. Inserting into a std::map returns an iterator and a bool. Writing a separate struct for every such case adds clutter; using two out-parameters mangles the function signature. std::pair is the standard library's "two-tuple" type for these cases.
The two members are literally named first and second. There's no third name. That's a feature: when you see a pair, you know which fields it has. It's also a limitation, because first and second say nothing about what they hold. A std::pair<std::string, double> could be name and price, or it could be email and last-login timestamp. Code reading the pair has to look at the surrounding context to know which.
std::pair lives in <utility>. You don't need to include it explicitly if you've already pulled in <map> or several other standard headers, but adding it directly is cleaner when using pair on its own.
The most explicit form is the two-argument constructor.
Brace initialization works too and is often shorter when the types are already clear from the variable's declared type.
When you don't want to spell the template arguments out, std::make_pair deduces them from the arguments you pass.
Since C++17, the constructors of std::pair themselves can deduce their template arguments, so you can write std::pair entry{"Mouse Pad", 14.99} directly. The catch is that string literals deduce as const char*, not std::string. For a std::string field, wrap the literal explicitly: std::pair entry{std::string("Mouse Pad"), 14.99}. The same trap exists with std::make_pair. Most code uses std::pair<std::string, double>{...} so the field types are pinned down at the declaration.
std::pair<T1, T2> is two members laid out side by side, plus any padding the compiler inserts for alignment. There's no extra header, no hidden allocation. Copying a pair copies both members; moving it moves both. The cost mirrors the cost of its parts.
C++ programmers end up using std::pair because the standard library returns it from common operations. std::map<K, V>::insert returns a pair<iterator, bool>: the iterator points to the inserted (or existing) element, and the bool says whether the insertion happened.
The second insert does not overwrite the value, because the key already exists. The bool flag (.second on the returned pair) distinguishes the two cases. first.first is the iterator; first.first->second is the value stored in the map (the iterator points to a pair<const string, int>, and .second on that pair is the int). The double second chain is a small but real source of confusion when working with maps heavily.
Iterating a map gives a stream of these pair<const Key, Value> objects, one per entry. Before C++17, the canonical loop looked like this.
Map ordering is by key, which is why these come out alphabetically. The pair is hidden behind the iterator: it->first is the key, it->second is the value. The names first and second make the loop body harder to read. Structured bindings, covered at the end of this chapter, replace them with real names.
std::pair defines ==, !=, <, <=, >, >= lexicographically. The first fields are compared first; if they're equal, the second fields decide. This is how the language can sort containers of pairs without you writing a comparator.
Orders are sorted by priority first (the int), and pairs with the same priority break ties by product name (alphabetical, because std::string compares lexicographically). This "sort by primary, tie-break by secondary" pattern follows directly from pair's built-in ordering, which is why pairs show up often in algorithm problems.
Lexicographic comparison short-circuits. If the first members differ, the second members are never compared. Putting the cheaper-to-compare field first can save work when comparing pairs of large strings.
Two fields cover many cases, but not all of them. A function that looks up a product might return the name, the price, and the stock count. Three fields. std::pair won't do that on its own. You'd have to nest one inside another, ending up with pair<string, pair<double, int>> and a second.first/second.second access pattern that's hard to read.
std::tuple<Ts...> is the general N-field version. It accepts any number of types and gives you indexed access into the fields.
std::get<N>(t) returns a reference to the N-th field, where N is a compile-time constant. The index is zero-based. If you pass an index that's out of range, the program doesn't compile, which is better than a runtime error.
std::tuple lives in <tuple>. Include the header explicitly when using tuples directly.
The three forms mirror the pair patterns.
std::make_tuple deduces the types from the arguments. Same string-literal caveat as make_pair: a bare "USB Cable" would deduce as const char*. Wrap with std::string(...) for a std::string field, or specify the template arguments explicitly.
One quirk: std::make_tuple decays its arguments, meaning it strips references and top-level const. For a tuple of references, use std::tie or std::forward_as_tuple. std::tie is the more common one, covered shortly.
A std::tuple<Ts...> is roughly the size of its fields combined, plus any padding for alignment. The standard doesn't require a specific layout (so don't rely on field order in memory), but tuples don't add overhead beyond their members.
std::get<N>(t) returns the N-th field by index. std::get<T>(t) returns the field of type T, but only if exactly one field has that type.
By-type access is convenient when the types in your tuple are distinct and meaningful. It falls apart fast when two fields share a type.
The compiler refuses with an error along the lines of "more than one element of type int". In that case you fall back to indexed access. This is one reason a named struct often wins for tuples with repeated types: a struct with fields int x and int y is unambiguous in a way tuple<int, int> is not.
std::tie builds a tuple of references. Its main use is unpacking a tuple into separate variables.
std::tie(name, price, stock) returns a tuple<string&, double&, int&>. Assigning a tuple<string, double, int> into it copies each field into the referenced variable. The result is that name, price, and stock end up holding the three returned values. The variables have to exist before you call tie.
The other place std::tie shows up is comparison chains. For a class with three fields that needs to be sorted by all three lexicographically:
std::tie builds two tuples of references and lets operator< walk them lexicographically. The first differing field decides. Sort-by-multiple-keys works without a chain of nested if-statements.
std::tie can also throw away fields with std::ignore. If findProduct() returns three values and only the price matters, write std::tie(std::ignore, price, std::ignore) = findProduct();. The matching positions are dropped.
std::tie does not copy anything. It builds a tuple of references in place. The assignment copies the right-hand side into the referenced variables; that's where the work happens.
Before C++17, std::tie was the usual way to destructure a tuple. C++17 added a cleaner syntax called structured bindings.
auto [name, price, stock] = ...; declares three new variables and binds them to the three fields. No pre-declaration, no std::ignore, no std::tie. The same syntax works on pairs.
The map loop no longer ends in .first and .second. The names product and count read like a real record. This is one of the larger quality-of-life wins C++17 delivered for everyday code.
Structured bindings also work on plain structs and on arrays. The modern-cpp section has its own chapter on the details (forwarding references, const correctness, the underlying tuple_size protocol). For this chapter, auto [a, b, c] = tup; is the preferred unpack in any code targeting C++17 or later. std::tie still has its place in comparison chains and in pre-C++17 codebases.
Two small standard library traits round out the tuple toolkit. They're mostly used in generic code.
std::tuple_size<TupleType>::value gives the number of fields at compile time.
std::tuple_element<N, TupleType>::type gives the type of the N-th field.
Both traits play a role in how structured bindings work internally (the compiler queries tuple_size and tuple_element to know how many bindings to introduce and what their types are). They're rarely used directly, except in generic code that iterates over tuple fields.
Like pair, tuple defines all six comparison operators, and they're lexicographic. First field first, then second, and so on. This powers the std::tie comparison shown above.
a < b is true because the first two fields tie and the third decides. b < c is true because the first field decides. c < a is false for the same reason in reverse.
Same as pair: lexicographic compare short-circuits at the first differing field. Put cheap fields first when ordering matters.
This question is worth thinking about whenever you're about to declare a pair or a tuple. The answer is usually "named struct", and pair/tuple fit specific cases where the named struct would be overkill or wouldn't work at all.
| Choice | When it fits |
|---|---|
| Named struct | The fields have a meaning worth giving names to, and the type is used in more than one place. |
std::pair | Returning two values from a one-off function, or using a standard library API that returns a pair. |
std::tuple | Writing generic code that needs to handle a variable number of fields, or assembling a quick ad-hoc bundle of three or more values for one specific use. |
Three ways of returning a name, a price, and a stock count:
At the call site with structured bindings, the tuple version looks similar to the struct version. The difference shows up everywhere else. With the struct, ProductInfo has a name, lives in a header, and can be passed by name to other functions. The name conveys meaning. With the tuple, the return type is std::tuple<std::string, double, int>, which says nothing about meaning, and code passing it around has to keep the field order straight.
A few practical rules of thumb:
tuple<...> type signature in multiple places.map::insert, for instance), pair is fine.std::ignore for half the fields, the tuple is probably the wrong shape; either split it up or use a struct.std::tuple is the backbone of variadic-template plumbing.The named struct is often the more readable choice. Pair and tuple fit cases where readability of the fields is less important than getting the type built quickly, or where the standard library returns one.
The decision tree is short. Most of the time the answer is at the bottom-right corner: write a struct. Pair and tuple fit the edges of that decision.
A common modern pattern is returning a tuple and immediately unpacking it with structured bindings. The function is short to write and the call site reads with named locals.
This is a common use of tuple in modern C++. The function signature is short, the call site reads naturally, and the field names live at the place that uses them rather than in a header. The downside is that if a third function also needs to return (found, price, stock), define a struct so the three call sites can all use the same names.
Returning by tuple is no more expensive than returning multiple values through a struct. Both move (or copy) each field once. Modern compilers apply named return value optimisation to both forms when the return value is built from a brace-list.
Pairs and tuples interact cleanly with the algorithms in <algorithm>. Sorting, searching, and finding extrema all work as soon as the contained types are comparable. We already saw std::sort on a vector of pairs. std::max_element finds the most expensive product, using a custom comparator built on std::tuple so the tie-breaker is the name.
max_element walks the vector and returns an iterator to the tuple that compares "less than" all others under the given comparator. The comparator uses std::tie to chain two fields with mixed sort direction (the reversed argument order on the second tie inverts the comparison on name).
This kind of code is fiddly to read, which is a sign to step back and ask whether std::tuple<std::string, double, int> should have been a struct Product { std::string name; double price; int stock; }; from the start. Pair and tuple work for one-shot algorithmic work; they age quickly when the same shape is used throughout an application.
9 quizzes