C++ / STANDARD CONTAINERS
pair, tuple, and structured bindings
Bundle values with std::pair and std::tuple, unpack them with structured bindings, and judge when a named struct is the better choice.
What you will learn
- Unpack pairs, tuples, arrays, and plain structs with one auto [a, b] = ... line
- Pick auto, auto&, or const auto& to control whether bindings alias the original
- Use std::tie with std::ignore to fill existing variables and to sort by several keys
- Read pair<iterator, bool> results such as set::insert without .first and .second
Understanding pair, tuple, and structured bindings
std::pair and std::tuple are the standard library's way to glue a few values together without inventing a type for them. A pair gives you .first and .second; a tuple drops names entirely and is addressed by position with std::get<0>, std::get<1>, and so on. That index is a template argument rather than a runtime value, because every element may have a different type and the compiler has to know which type the access yields. That is the whole trade: you skip writing a struct, and in exchange the members lose their meaning at the point of use.
Structured bindings, added in C++17, hand readable names back to those anonymous members. auto [q, r] = divmod(a, b); does not declare two variables; it creates one invisible object initialized from the right-hand side and then makes q and r names for that object's first and second member. Once you hold that model the rules stop looking arbitrary: auto copies the source into the hidden object while auto& makes the hidden object a reference into the source, and the number of names must match the element count exactly because you are naming every member, not selecting some. The same syntax works on three kinds of source: raw arrays, tuple-like types that supply std::tuple_size and get, and plain structs whose non-static data members are all public and declared in one class.
Pairs and tuples earn their place where a grouping is temporary and local, and in library APIs that must return two things at once, such as set::insert returning pair<iterator, bool> or std::minmax_element returning two iterators. Their element-by-element comparison is a second reason to reach for them: std::tie(a.year, a.city) < std::tie(b.year, b.city) is a correct two-key comparator in one line instead of a chain of if statements. When the group starts crossing an API boundary, or when a reader needs a comment to know what .second holds, define a struct with real field names; callers still unpack it with the same structured binding and lose nothing.
<iostream>
<string>
<tuple>
<utility>
std::pair<int, std::string> divide(int a, int b) {
return {a / b, a % b == 0 ? "exact" : "remainder"};
}
std::tuple<std::string, int, double> item() {
return {"widget", 4, 2.5};
}
int main() {
auto [quotient, kind] = divide(17, 5);
std::cout << quotient << ' ' << kind << '\n';
auto [name, count, price] = item();
std::cout << name << " x" << count << " @ " << price << '\n';
std::tuple<std::string, int, double> t = item();
std::cout << std::get<0>(t) << ' ' << std::get<int>(t) << ' '
<< std::tuple_size_v<decltype(t)> << '\n';
std::pair<int, char> a{1, 'z'}, b{1, 'a'};
std::cout << (a < b) << ' ' << (b < a) << '\n';
}
A structured binding creates no new variables of its own; it names the members of one hidden object, so auto versus auto& decides whether you are looking at a copy or at the original.
Worked examples
Copy bindings versus reference bindings
Shows that auto binds names into a copy of the source while auto& binds them into the source itself.
<iostream>
<string>
<utility>
<vector>
int main() {
std::vector<std::pair<std::string, int>> stock{{"bolt", 3}, {"nut", 7}};
auto [item, qty] = stock[0]; // hidden object is a copy
qty = 99;
std::cout << item << ": vector " << stock[0].second << ", copy " << qty << '\n';
auto& [name, count] = stock[1]; // hidden object is a reference
count = 99;
std::cout << name << ": vector " << stock[1].second << '\n';
for (const auto& [k, v] : stock)
std::cout << k << '=' << v << '\n';
}
Example explained
Line 1auto [item, qty] = stock[0]; copies the whole pair, so qty = 99 writes into the copy and the vector still holds 3.
Line 2auto& [name, count] = stock[1]; makes the hidden object a reference, so count is exactly stock[1].second and the write sticks.
Line 3qty has no storage of its own: it is a name for a member of the hidden copy, which is why & on the declaration changes the meaning of every name in the list.
Line 4const auto& in the range-for avoids copying each pair per iteration and forbids writes through k and v.
std::tie for multi-key comparison and for existing variables
Uses std::tie to sort by two keys and to assign into variables that already exist, discarding one element with std::ignore.
<algorithm>
<iostream>
<string>
<tuple>
<vector>
struct Row { std::string city; int year; double temp; };
bool byYearThenCity(const Row& a, const Row& b) {
return std::tie(a.year, a.city) < std::tie(b.year, b.city);
}
int main() {
std::vector<Row> rows{{"Oslo", 2021, 5.5}, {"Bern", 2021, 9.0}, {"Lima", 2019, 8.0}};
std::sort(rows.begin(), rows.end(), byYearThenCity);
for (const auto& [city, year, temp] : rows)
std::cout << year << ' ' << city << ' ' << temp << '\n';
std::string c;
int y = 0;
std::tie(c, y, std::ignore) = std::make_tuple(std::string("Rome"), 2024, 1.0);
std::cout << c << ' ' << y << '\n';
}
Example explained
Line 1std::tie(a.year, a.city) builds a tuple of references, and tuple comparison is lexicographic: year decides first, city breaks ties.
Line 2const auto& [city, year, temp] binds to a plain struct, not a tuple, because all three members are public and declared in one class.
Line 3std::tie(c, y, std::ignore) makes a tuple of references to existing variables, so the assignment writes into c and y instead of declaring new names.
Line 4std::ignore has an assignment operator that accepts and discards anything, which is how the 1.0 is dropped.
Unpacking pairs returned by the library
Reads the pair results of std::minmax_element and set::insert through structured bindings.
<algorithm>
<iostream>
<set>
<vector>
int main() {
std::vector<int> v{4, 1, 9, 1, 7};
auto [lo, hi] = std::minmax_element(v.begin(), v.end());
std::cout << *lo << ' ' << *hi << '\n';
std::set<int> s{1, 2};
auto [it, inserted] = s.insert(2);
std::cout << *it << ' ' << inserted << '\n';
auto [it2, added] = s.insert(5);
std::cout << *it2 << ' ' << added << '\n';
}
Example explained
Line 1minmax_element returns a pair of iterators, so lo and hi are iterators and still need dereferencing to print values.
Line 2s.insert(2) finds 2 already present: the iterator points at the existing element and the bool is false, printed as 0.
Line 3s.insert(5) succeeds, so the bool is true and prints as 1; add std::boolalpha if you want the words.
Line 4Both bindings must be declared fresh, which is why the second call uses new names it2 and added rather than reusing it and inserted.
Important notes
Structured bindings require -std=c++17 or newer; std::tie into pre-declared variables was the C++11 workaround, and using a binding name inside a lambda capture only became legal in C++20.
Binding to a struct works only if every non-static data member is public and declared in the same class, and you must name all of them; there is no std::ignore for structured bindings, so use a throwaway name.
Common mistakes
Writing for (auto [name, n] : v) { n = 0; } and expecting v to change: each iteration binds into a fresh copy of the element, so the container is untouched and every element is copied. Use auto& [name, n].
Returning std::tie(localA, localB) instead of std::make_pair or std::make_tuple: tie produces references to locals that die at the return, so the caller reads dangling references.
Reaching for std::get<i>(t) with a loop variable i: the index is part of the type, so it must be a compile-time constant, and a runtime index into a tuple does not compile at all.
Try it yourself
Change, predict, then run
Write std::pair<int, int> divmod(int a, int b) and print the quotient and remainder of 47 and 5 using auto [q, r] = divmod(47, 5);. Then change the return type to struct DivResult { int quotient; int remainder; }; and confirm the call site compiles without any edit.
Open the C++ workspaceCheck your understanding
std::string city; int year; already exist, and f() returns std::tuple<std::string, int, double>. You want to fill those two variables and discard the double. Which line does that?
- std::tie(city, year, std::ignore) = f();
- auto [city, year, unused] = f();
- auto& [city, year, unused] = f();
- std::make_tuple(city, year, std::ignore) = f();
Show answer
std::tie builds a tuple of references to city and year, so assigning a tuple into it writes through to the variables you already have, and std::ignore's assignment operator swallows the double. Option 1 looks equivalent but a structured binding always declares new names, so in a scope where city and year exist it is a redeclaration error, and elsewhere it would fill different variables; option 3 copies the current values into a temporary tuple that is then thrown away.