Tuples and pairs

A function returns one value, and often the program needs two: a result and a status, a minimum and a maximum, a key and a payload. A pair holds two values, a tuple holds any number, and both are ordinary value types that copy, compare and pass by value.

They are the lightweight answer to multiple return values. A struct gives the members names and is usually clearer for a public interface; a pair or a tuple is right for a local result that is unpacked immediately, which is what the structured binding of lesson 021 does.

std::pair

A pair has the members first and second, and make_pair deduces the two types from its arguments. The comparison operators compare first and then second, which makes a pair sortable as a key.

The program builds a pair from a string and an int, prints the members and compares two pairs. The second pair is smaller because its first member comes first in the alphabet.

A pair is what a map stores, which is why the loops of lesson 054 unpack one with a structured binding. The member names first and second carry no meaning, so a named struct is better once the pair travels through an interface.

The comparison is lexicographic: first decides, and second only matters when the first members are equal. That is what makes a pair a usable key in a std::map or a std::set.

#include <iostream> #include <string> #include <utility> int main() { std::pair<std::string, int> entry{"ada", 36}; auto other = std::make_pair(std::string{"bob"}, 41); std::cout << entry.first << ' ' << entry.second << '\n'; std::cout << (entry < other) << '\n'; }

g++ -std=c++23 -Wall -Wextra pair.cpp -o pair_demo
./pair_demo

ada 36
1

std::tuple

A tuple holds any number of values of any types. std::get<Index> reads one by position and std::get<Type> reads the only element of that type, if there is exactly one.

The program builds a tuple of an int, a string and a double and prints the three members. The index is a compile-time constant, so the access costs nothing and a wrong index is a compile error.

std::tuple_size and std::tuple_element answer the size and the type of a member at compile time. Lesson 048 showed the index sequence that makes a loop over a tuple possible.

#include <iostream> #include <string> #include <tuple> int main() { std::tuple<int, std::string, double> record{7, "seven", 7.5}; std::cout << std::get<0>(record) << ' ' << std::get<1>(record) << ' ' << std::get<2>(record) << '\n'; static_assert(std::tuple_size_v<decltype(record)> == 3); }

g++ -std=c++23 -Wall -Wextra tuple.cpp -o tuple_demo
./tuple_demo

7 seven 7.5

std::tie and returning two values

std::tie builds a tuple of references, so an assignment unpacks into existing variables. The program's min_max returns a pair and main unpacks it with std::tie, which is the pre-C++17 form of a structured binding.

A function that returns a pair is the standard way to return two values without an output parameter. The pair is a value, so the caller may ignore one member or copy the whole result.

std::ignore lets one member of the tuple be skipped in a tie assignment. With a structured binding the same thing is written by naming the parts, and lesson 021 prefers that form in new code.

#include <iostream> #include <tuple> #include <utility> std::pair<int, int> min_max(int a, int b) { if (a < b) { return {a, b}; } return {b, a}; } int main() { int low = 0; int high = 0; std::tie(low, high) = min_max(9, 4); std::cout << low << ' ' << high << '\n'; }

g++ -std=c++23 -Wall -Wextra tie.cpp -o tie_demo
./tie_demo

4 9

std::apply

std::apply calls a function with the members of a tuple as its arguments. It is how a tuple becomes a call, and it removes the need to write get<0>, get<1> and so on for each position.

The program's print_point takes two ints and apply passes the two members of the tuple. The function can be a lambda, a function pointer or a function object.

The mechanism is the index sequence of lesson 048: apply builds one for the tuple's size and expands it into the call. It is how std::make_from_tuple constructs an object from a tuple.

#include <iostream> #include <string> #include <tuple> void print_point(int x, int y) { std::cout << '(' << x << ", " << y << ')' << '\n'; } int main() { std::tuple<int, int> point{3, 4}; std::apply(print_point, point); }

g++ -std=c++23 -Wall -Wextra apply.cpp -o apply_demo
./apply_demo

(3, 4)

Too many names in a binding

A structured binding must name exactly as many parts as the tuple has. The program binds three names to a pair, and the compiler reports that only two are available.

The message says the number of names does not match the number of elements and points at the declaration. The fix is to drop a name or to use a tuple with three members.

The same check applies to a struct: the number of names must equal the number of non-static data members, and a class with private members cannot be bound at all.

#include <utility> int main() { std::pair<int, int> value{1, 2}; auto [a, b, c] = value; return a + b + c; }

g++ -std=c++23 -Wall -Wextra too_many.cpp -o too_many

too_many.cpp: In function 'int main()':
too_many.cpp:6:10: error: 3 names provided for structured binding
6 | auto [a, b, c] = value;
| ^~~~~~~~~
too_many.cpp:6:10: note: while 'std::pair<int, int>' decomposes into 2 elements

Exercises

  1. Write a function that returns a pair of the smallest and largest element of a vector.
  2. Unpack a tuple of three values with std::tie and with a structured binding, and compare the two.
  3. Use std::apply to call a lambda that sums the three members of a tuple.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How many values does std::pair hold?

    Answer

    Two

    The pair is the two-value case of the tuple.

  2. What does std::tie allow?

    Answer

    Binding existing variables to the parts of a tuple

    tie(a, b) = make_tuple(...) assigns the parts to the variables.

  3. What does std::apply do?

    Answer

    It calls a function with the tuple's elements as arguments

    The tuple is unpacked into the call's argument list.

  4. What does std::get<1> do on a tuple?

    Answer

    It returns the element at index 1

    The index is a template argument and is checked at compile time.

  5. What does this program print? #include <iostream> #include <string> #include <utility> int main() { std::pair<std::string, int> person{"ada", 36}; std::cout << person.first << ' ' << person.second << '\n'; std::cout << (person == std::pair<std::string, int>{"ada", 36}); }

    Answer

    ada 36 then 1

    The two members are printed, and the pairs compare equal member by member.

  6. What does this program print? #include <iostream> #include <string> #include <tuple> int main() { std::tuple<int, std::string, double> item{7, "seven", 7.5}; std::cout << std::get<0>(item) << ' ' << std::get<1>(item) << ' ' << std::get<2>(item); }

    Answer

    7 seven 7.5

    The tuple holds three heterogeneous values, and get reads each by index.

  7. What does this program print? #include <iostream> #include <tuple> std::pair<int, int> min_max(int a, int b) { return a < b ? std::pair<int, int>{a, b} : std::pair<int, int>{b, a}; } int main() { int low = 0, high = 0; std::tie(low, high) = min_max(9, 4); std::cout << low << ' ' << high; }

    Answer

    4 9

    tie assigns the two returned values to the two variables, and the smaller is 4.

  8. What does this program print? #include <iostream> #include <tuple> struct point { int x; int y; }; int main() { auto describe = [](point p) { std::cout << '(' << p.x << ", " << p.y << ')'; }; std::apply(describe, std::make_tuple(point{3, 4})); }

    Answer

    (3, 4)

    apply passes the tuple's single element to the lambda, which prints the two fields.

  9. What does the compiler report for this binding? #include <utility> int main() { std::pair<int, int> value{1, 2}; auto [a, b, c] = value; }

    Answer

    3 names provided for structured binding

    The pair has two elements, and the note says how many the binding must name.

  10. Why prefer returning a tuple over an out parameter?

    Answer

    The result is a value that can be stored and passed on

    A returned tuple keeps the two results together and needs no reference parameter.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev

Banner image