Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 5 – Arrays, vectors and aggregate types

std::pair, std::tuple and structured bindings

Group values with std::pair and std::tuple, unpack them with structured bindings, avoid hidden copies in map loops and sort by several keys with std::tie.

  • Beginner
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 20 minutes

What you will learn

  • Group related values with std::pair and std::tuple
  • Write structured bindings that unpack pairs, tuples, arrays and structs
  • Sort records by several keys with std::tie and tuple comparison
  • Choose between a pair, a tuple and a named struct for a result

Before you start

What you’ll learn · Before you start

What you will learn

  • Group related values with std::pair and std::tuple
  • Write structured bindings that unpack pairs, tuples, arrays and structs
  • Sort records by several keys with std::tie and tuple comparison
  • Choose between a pair, a tuple and a named struct for a result

Before you start

On this page

std::pair holds two values, called first and second; std::tuple holds any fixed number of values, each of its own type. Both group values without declaring a struct, which suits short-lived results: a function that returns a name and a score, or an element of a std::map, which is a pair of a key and its value. Structured bindings, added in C++17, unpack such a group into names in one declaration, auto [name, marks] = record;, and they work for arrays and for structs with public members too. When the values mean something beyond the next line or two, a struct with named members is still the clearer choice.

This lesson builds pairs and tuples, shows what a structured binding really declares, measures the copies that the wrong loop over a map makes, sorts records by several keys with std::tie, and ends with two C++26 additions. Every example runs with Apple clang 21 and GCC 16.

std::pair holds two values

std::pair, from <utility>, is a struct with two public members, first and second, and their types are its template arguments:

pair_basics.cpp C++ · pair_basics.cpp
// std::pair holds two values, called first and second.
#include <algorithm>
#include <map>
#include <print>
#include <string>
#include <type_traits>
#include <utility>

int main() {
    std::pair<std::string, int> top{"Asha", 91};
    std::println("{} scored {}", top.first, top.second);

    std::pair guessed{"Asha", 91};   // the compiler deduces the types from the values
    std::println("pair<const char*, int>: {}", std::is_same_v<decltype(guessed), std::pair<const char*, int>>);

    using namespace std::string_literals;
    std::pair fixed{"Asha"s, 91};    // "Asha"s is a std::string
    std::println("pair<std::string, int>: {}", std::is_same_v<decltype(fixed), std::pair<std::string, int>>);

    std::map<std::string, int> marks{{"Asha", 91}};
    auto result = marks.insert({"Asha", 75});           // insert returns an iterator and a bool
    std::println("inserted: {}, value kept: {}", result.second, result.first->second);

    auto range = std::minmax({72, 91, 85, 64});         // so does std::minmax: the smallest and the largest
    std::println("lowest {}, highest {}", range.first, range.second);
}
Pairs, built and run with both compilers Python · pair_basics_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra pair_basics.cpp -o pair_basics && ./pair_basics
g++-16 -std=c++23 -Wall -Wextra pair_basics.cpp -o pair_basics-gcc && ./pair_basics-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra pair_basics.cpp -o pair_basics && ./pair_basics
Asha scored 91
pair<const char*, int>: true
pair<std::string, int>: true
inserted: false, value kept: 91
lowest 64, highest 91
$ g++-16 -std=c++23 -Wall -Wextra pair_basics.cpp -o pair_basics-gcc && ./pair_basics-gcc
Asha scored 91
pair<const char*, int>: true
pair<std::string, int>: true
inserted: false, value kept: 91
lowest 64, highest 91

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 pair_basics_session.py

Three things are worth noticing in the output:

  • Without template arguments, the compiler deduces the types from the values, and a string literal such as "Asha" is a const char*, not a std::string. Comparing two such pairs compares addresses, not text. Write the types out, or use the s suffix from std::string_literals, which makes "Asha"s a std::string.
  • The standard library returns pairs where it has two results. std::map::insert returns the position of the element and whether it was inserted: here false, because the key "Asha" was already there, so the old value 91 stays.
  • std::minmax returns the smallest and the largest value as a pair, found in one pass.

Pairs compare like words in a dictionary: first decides, and second only when the firsts are equal.

std::tuple holds any number of values

std::tuple, from <tuple>, generalises the pair to any number of elements. There are no member names, so you reach an element by its position with std::get<0>(t), or by its type, std::get<double>(t), when that type occurs only once in the tuple:

tuple_basics.cpp C++ · tuple_basics.cpp
// std::tuple holds a fixed number of values, each with its own type.
#include <print>
#include <string>
#include <tuple>

std::tuple<std::string, int, double> topper() {
    return {"Asha", 12, 91.5};
}

int main() {
    auto t = topper();
    std::println("{} (roll {}): {}", std::get<0>(t), std::get<1>(t), std::get<2>(t));
    std::println("marks, found by type: {}", std::get<double>(t));
    std::println("elements: {}", std::tuple_size_v<decltype(t)>);

    std::string name;
    int roll = 0;
    std::tie(name, roll, std::ignore) = topper();   // assigns to variables that already exist
    std::println("{} has roll number {}", name, roll);
}
A tuple returned from a function Python · tuple_basics_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra tuple_basics.cpp -o tuple_basics && ./tuple_basics
g++-16 -std=c++23 -Wall -Wextra tuple_basics.cpp -o tuple_basics-gcc && ./tuple_basics-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra tuple_basics.cpp -o tuple_basics && ./tuple_basics
Asha (roll 12): 91.5
marks, found by type: 91.5
elements: 3
Asha has roll number 12
$ g++-16 -std=c++23 -Wall -Wextra tuple_basics.cpp -o tuple_basics-gcc && ./tuple_basics-gcc
Asha (roll 12): 91.5
marks, found by type: 91.5
elements: 3
Asha has roll number 12

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 tuple_basics_session.py

std::tuple_size_v gives the number of elements at compile time. std::tie(name, roll, std::ignore) makes a tuple of references to existing variables, so assigning a tuple to it copies each element into its variable, and std::ignore throws one element away. This was the usual way to unpack a returned tuple before C++17.

Positions such as std::get<2>(t) say nothing about what the value is, and a reader has to look up the function to learn which element is which. That is the problem structured bindings solve.

Structured bindings unpack in one declaration

A structured binding declares a name for every element of an object at once:

bindings.cpp C++ · bindings.cpp
// Structured bindings name each element of a struct, an array or a tuple in one declaration.
#include <print>
#include <string>
#include <tuple>

struct Student {
    std::string name;
    int marks = 0;
};

int main() {
    Student s{"Meera", 85};

    auto [name, marks] = s;            // names for the members of a copy of s
    marks = 50;                        // changes the copy only
    std::println("{}: copy {}, original {}", name, marks, s.marks);

    auto& [same_name, same_marks] = s;   // names for the members of s itself
    same_marks = 95;
    std::println("{}: original now {}", same_name, s.marks);

    int sides[3]{3, 4, 5};
    auto [a, b, c] = sides;            // an array
    std::println("{} + {} > {}: {}", a, b, c, a + b > c);

    auto [who, roll, score] = std::tuple{std::string{"Dev"}, 7, 72.5};   // a tuple
    std::println("{} (roll {}): {}", who, roll, score);
}
Copies, references, an array and a tuple Python · bindings_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra bindings.cpp -o bindings && ./bindings
g++-16 -std=c++23 -Wall -Wextra bindings.cpp -o bindings-gcc && ./bindings-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra bindings.cpp -o bindings && ./bindings
Meera: copy 50, original 85
Meera: original now 95
3 + 4 > 5: true
Dev (roll 7): 72.5
$ g++-16 -std=c++23 -Wall -Wextra bindings.cpp -o bindings-gcc && ./bindings-gcc
Meera: copy 50, original 85
Meera: original now 95
3 + 4 > 5: true
Dev (roll 7): 72.5

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bindings_session.py

auto [name, marks] = s; does not make two independent variables. It declares one unnamed variable initialised from s, here a copy of it, and makes name and marks names for the members of that unnamed variable. That is why assigning 50 to marks changed the copy and left s.marks at 85. With auto&, the unnamed variable is a reference to s, so same_marks = 95 changed s itself, and const auto& gives read-only names without a copy.

With auto the names refer to members of an unnamed copy of s; with auto& the unnamed variable is a reference to s itself.auto [name, marks] = s;auto& [name, marks] = s;Student sname = "Meera"marks = 85an unnamed copy of sname = "Meera"marks = 85name and marksrefer to its membersan unnamed reference to sname and marksrefer to the members of scopied fromis

What a structured binding declares, with auto and with auto&

Text description of the diagram

The diagram compares two declarations that unpack the same Student s, whose name is "Meera" and whose marks are 85.

  1. auto [name, marks] = s; creates an unnamed variable that is a copy of s. The names name and marks refer to the members of that copy, so changing marks leaves s unchanged.
  2. auto& [name, marks] = s; creates an unnamed reference to s. The names name and marks then refer to the members of s itself, so assigning to marks changes s.

In both cases name and marks are not separate variables of their own: they are names for the members of the unnamed variable.

A structured binding can unpack three kinds of object:

  • an array, such as int sides[3], whose size the compiler knows;
  • a tuple-like type: std::pair, std::tuple, std::array, and any type that describes its elements through std::tuple_size, std::tuple_element and get;
  • a class whose non-static data members are all public, such as the aggregates of the previous lesson, with one name per member in declaration order (inside the class’s own member functions, private members count too).

The number of names must equal the number of elements exactly. A std::vector cannot be unpacked, because its size is known only at run time, and outside its own members a class with a private data member cannot either.

Loops over a map without copies

A std::map<std::string, Marks> stores its elements as std::pair<const std::string, Marks>: the key is const, so that nobody can change it and break the map’s order. A range-based for loop with structured bindings is the natural way to read them, but the declaration decides whether every element is copied. Marks below counts its own copies:

map_copies.cpp C++ · map_copies.cpp
// How many copies does each loop over a map make? Marks counts every copy of itself.
#include <map>
#include <print>
#include <string>
#include <utility>

struct Marks {
    int value = 0;
    static inline int copies = 0;
    Marks(int v) : value{v} {}
    Marks(const Marks& other) : value{other.value} { ++copies; }
};

int main() {
    std::map<std::string, Marks> marks{{"Asha", 91}, {"Dev", 72}, {"Meera", 85}};
    int total = 0;

    Marks::copies = 0;
    for (auto [name, m] : marks) total += m.value;
    std::println("auto [name, m]                    {} copies", Marks::copies);

    Marks::copies = 0;
    for (const auto& [name, m] : marks) total += m.value;
    std::println("const auto& [name, m]             {} copies", Marks::copies);

    Marks::copies = 0;
    for (const std::pair<std::string, Marks>& p : marks) total += p.second.value;
    std::println("const pair<string, Marks>&        {} copies", Marks::copies);

    Marks::copies = 0;
    for (const std::pair<const std::string, Marks>& p : marks) total += p.second.value;
    std::println("const pair<const string, Marks>&  {} copies", Marks::copies);

    for (auto& [name, m] : marks) m.value += 5;   // the values can change; the keys are const
    std::println("Asha now has {} (the four loops added up {})", marks.at("Asha").value, total);
}
Copies made by four loops over three elements Python · map_copies_session.py
from session import run

# -Wall makes both compilers point at the loop that copies without saying so.
run("""
clang++ -std=c++23 -Wall -Wextra map_copies.cpp -o map_copies && ./map_copies
g++-16 -std=c++23 -Wall -Wextra map_copies.cpp -o map_copies-gcc && ./map_copies-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra map_copies.cpp -o map_copies && ./map_copies
map_copies.cpp:27:47: warning: loop variable 'p' of type 'const std::pair<std::string, Marks> &' (aka 'const pair<basic_string<char>, Marks> &') binds to a temporary constructed from type 'reference' (aka 'pair<const std::string, Marks> &') [-Wrange-loop-construct]
   27 |     for (const std::pair<std::string, Marks>& p : marks) total += p.second.value;
      |                                               ^
map_copies.cpp:27:10: note: use non-reference type 'std::pair<std::string, Marks>' (aka 'pair<basic_string<char>, Marks>') to make construction explicit or type 'const value_type &' (aka 'const pair<const std::string, Marks> &') to prevent copying
   27 |     for (const std::pair<std::string, Marks>& p : marks) total += p.second.value;
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
auto [name, m]                    3 copies
const auto& [name, m]             0 copies
const pair<string, Marks>&        3 copies
const pair<const string, Marks>&  0 copies
Asha now has 96 (the four loops added up 992)
$ g++-16 -std=c++23 -Wall -Wextra map_copies.cpp -o map_copies-gcc && ./map_copies-gcc
map_copies.cpp: In function 'int main()':
map_copies.cpp:27:47: warning: loop variable 'p' of type 'const std::pair<std::__cxx11::basic_string<char>, Marks>&' binds to a temporary constructed from type 'std::pair<const std::__cxx11::basic_string<char>, Marks>' [-Wrange-loop-construct]
   27 |     for (const std::pair<std::string, Marks>& p : marks) total += p.second.value;
      |                                               ^
map_copies.cpp:27:47: note: use non-reference type 'const std::pair<std::__cxx11::basic_string<char>, Marks>' to make the copy explicit or 'const std::pair<const std::__cxx11::basic_string<char>, Marks>&' to prevent copying
auto [name, m]                    3 copies
const auto& [name, m]             0 copies
const pair<string, Marks>&        3 copies
const pair<const string, Marks>&  0 copies
Asha now has 96 (the four loops added up 992)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 map_copies_session.py

The counts are the same with both compilers:

  • auto [name, m] copies every element, key and value, into the unnamed variable: 3 copies.
  • const auto& [name, m] binds to each element where it is: no copy.
  • const std::pair<std::string, Marks>& looks like a reference, yet copies every element. The element type is a pair with a const std::string, a different type, so the compiler builds a temporary pair of the written type for each element and binds the reference to that. Both compilers point at this loop with -Wall (-Wrange-loop-construct).
  • Naming the right type, const std::pair<const std::string, Marks>&, avoids the copy, but const auto& is shorter and cannot get the type wrong.

To change the values in place, write auto&: for (auto& [name, m] : marks) m.value += 5; changes the map, while an attempt to assign to name does not compile, because the key is const. With a few strings the copies cost little, but in a loop over a large map of large values they cost real time, and nothing in the code says that they happen.

Sorting by several keys with std::tie

Tuples compare lexicographically: the first elements decide, and only when they are equal do the second elements, and so on. std::tie(a.marks, a.name) builds a tuple of references to the members, without copying them, so comparing two such tuples compares the records by marks and then by name. Swapping a and b in one position turns that key around:

rank_students.cpp C++ · rank_students.cpp
// Highest marks first, and names from A to Z among equal marks, by comparing std::tie tuples.
#include <algorithm>
#include <print>
#include <string>
#include <tuple>
#include <vector>

struct Student {
    std::string name;
    int marks = 0;
};

int main() {
    std::println("(2, 9) < (3, 0): {}", std::tuple{2, 9} < std::tuple{3, 0});   // first elements decide
    std::println("(3, 1) < (3, 0): {}", std::tuple{3, 1} < std::tuple{3, 0});   // a tie: the second decides

    std::vector<Student> students{{"Kabir", 72}, {"Asha", 91}, {"Dev", 72}, {"Meera", 85}, {"Arjun", 91}};
    std::ranges::sort(students, [](const Student& a, const Student& b) {
        // b's marks against a's: a larger mark sorts first. Then a's name against b's: A to Z.
        return std::tie(b.marks, a.name) < std::tie(a.marks, b.name);
    });
    for (const auto& [name, marks] : students) std::println("{:>3} {}", marks, name);
}
Highest marks first, then names from A to Z Python · rank_students_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra rank_students.cpp -o rank_students && ./rank_students
g++-16 -std=c++23 -Wall -Wextra rank_students.cpp -o rank_students-gcc && ./rank_students-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra rank_students.cpp -o rank_students && ./rank_students
(2, 9) < (3, 0): true
(3, 1) < (3, 0): false
 91 Arjun
 91 Asha
 85 Meera
 72 Dev
 72 Kabir
$ g++-16 -std=c++23 -Wall -Wextra rank_students.cpp -o rank_students-gcc && ./rank_students-gcc
(2, 9) < (3, 0): true
(3, 1) < (3, 0): false
 91 Arjun
 91 Asha
 85 Meera
 72 Dev
 72 Kabir

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rank_students_session.py

The comparison std::tie(b.marks, a.name) < std::tie(a.marks, b.name) puts b.marks on the left, so a student with more marks sorts first, and keeps a.name on the left, so equal marks fall back to alphabetical order: Arjun before Asha, Dev before Kabir. The previous lesson’s defaulted operator<=> always compares in declaration order, ascending; std::tie lets each sort choose its own keys and directions without changing the struct. It needs named variables or members, though: for a key that is computed, such as a total, build a std::tuple of values instead.

pair, tuple or a named struct?

All three group values, and all three work with structured bindings, so the choice is about the reader:

Choosing a way to group values
Criterion std::pairstd::tupleA struct
Names first, secondPositions: get<0> …Your own
Parts Exactly 2Any fixed numberAny
Compare Built inBuilt inWith = default
Unpack YesYesIf members are public
When to choose A key with its value; library resultsGeneric code; std::tie keysValues with a meaning

The C++ Core Guidelines (rule F.21) prefer a struct for a function that returns several values: Summary with members min, max and mean tells the caller what each value is, and a structured binding unpacks it just as easily as a tuple. Keep pairs and tuples for code that does not know what the values mean, such as templates that handle any number of types, and for keys and results whose meaning is obvious on the spot.

C++26: the placeholder _ and binding packs

C++26, whose draft is complete but not yet published as an ISO standard, adds two features to structured bindings. Both compilers accept them with -std=c++2c, the option for the C++26 draft:

  • A placeholder, _ (paper P2169R4), names an element you do not need. Unlike an ordinary name, it may appear several times in one declaration: auto [_, physics, _] = scores;. GCC has supported it since version 14 and Clang since version 18.
  • Binding packs (paper P1061R10) give a group of names to “all the remaining elements”: auto [first, ...rest] = value;. A pack works only inside a template, and you use it like any other pack, with sizeof... or a fold expression. GCC 16 and Clang 21 are the first versions with it.
cpp26_bindings.cpp C++ · cpp26_bindings.cpp
// C++26 previews: the placeholder _ and structured binding packs (build with -std=c++2c).
#include <print>
#include <tuple>

struct Scores {
    int maths = 0;
    int physics = 0;
    int chemistry = 0;
};

// A pack, ...parts, gets one name for every element, whatever the type has. Packs work only in templates.
template <class T>
int total(const T& value) {
    const auto& [...parts] = value;
    return (0 + ... + parts);
}

template <class T>
int count_after_first(const T& value) {
    const auto& [first, ...rest] = value;
    (void)first;
    return sizeof...(rest);
}

int main() {
    Scores s{91, 78, 85};
    auto [_, physics, _] = s;   // _ can be repeated: these elements are not needed
    std::println("physics {}", physics);
    std::println("total of a struct {}, of a tuple {}", total(s), total(std::tuple{1, 2, 3, 4}));
    std::println("elements after the first: {}", count_after_first(s));
}
Both compilers in C++26 mode Python · cpp26_bindings_session.py
from session import run

# -std=c++2c is the switch for the C++26 draft in both compilers.
run("""
clang++ -std=c++2c -Wall -Wextra cpp26_bindings.cpp -o cpp26_bindings && ./cpp26_bindings
g++-16 -std=c++2c -Wall -Wextra cpp26_bindings.cpp -o cpp26_bindings-gcc && ./cpp26_bindings-gcc
""")

Output

$ clang++ -std=c++2c -Wall -Wextra cpp26_bindings.cpp -o cpp26_bindings && ./cpp26_bindings
physics 78
total of a struct 254, of a tuple 10
elements after the first: 2
$ g++-16 -std=c++2c -Wall -Wextra cpp26_bindings.cpp -o cpp26_bindings-gcc && ./cpp26_bindings-gcc
physics 78
total of a struct 254, of a tuple 10
elements after the first: 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cpp26_bindings_session.py

total adds up the members of any struct, tuple or array without knowing how many there are. Before C++26 that took std::apply and a lambda for tuples, and for structs a library or a separate version for each number of members. An earlier revision of the paper also allowed packs outside templates; the final one does not, and both compilers reject auto [first, ...rest] in an ordinary function.

Your turn: one summary, two shapes

summary.h declares two functions that compute the smallest value, the largest and the mean of a list of integers. summarise returns the struct Summary; summarise_tuple returns the same results as a std::tuple<int, int, double>. Write both. You may assume the list is never empty.

The starter code and its sample tests

C header · summary.h

// Starter code for "Your turn": write summarise and summarise_tuple.
#pragma once
#include <tuple>
#include <vector>

struct Summary {
    int min = 0;
    int max = 0;
    double mean = 0.0;
    bool operator==(const Summary&) const = default;
};

// The smallest value, the largest and their mean. values is never empty.
inline Summary summarise(const std::vector<int>& values) {
    // TODO
    (void)values;
    return {};
}

// The same three results as a tuple, in the same order.
inline std::tuple<int, int, double> summarise_tuple(const std::vector<int>& values) {
    // TODO: a structured binding on summarise(values) makes this short
    (void)values;
    return {};
}

C++ · summary_tests.cpp

// Sample tests for summary.h. Build: clang++ -std=c++23 -Wall -Wextra summary_tests.cpp -o summary_tests
#include <print>
#include <tuple>
#include <vector>
#include "summary.h"

int checks = 0;
int failures = 0;

void check(bool ok, const char* what) {
    ++checks;
    if (!ok) {
        ++failures;
        std::println("FAIL {}", what);
    }
}

int main() {
    std::vector<int> marks{72, 91, 64, 85};
    check(summarise(marks) == Summary{.min = 64, .max = 91, .mean = 78.0}, "min, max and mean of 72, 91, 64, 85");
    check(summarise({-5}) == Summary{.min = -5, .max = -5, .mean = -5.0}, "one value is the min, the max and the mean");
    check(summarise({1, 2}).mean == 1.5, "the mean keeps its fraction");

    auto [low, high, mean] = summarise_tuple(marks);
    check(low == 64 && high == 91 && mean == 78.0, "the tuple holds min, max and mean, in that order");
    check(summarise_tuple({7}) == std::tuple{7, 7, 7.0}, "the tuple of one value");

    std::println("{} of {} checks passed", checks - failures, checks);
    return failures == 0 ? 0 : 1;
}
The sample tests against the starter code Python · summary_session.py
from session import run

# The sample tests run against the starter code, before any TODO is done.
run("""
clang++ -std=c++23 -Wall -Wextra summary_tests.cpp -o summary_tests && ./summary_tests
g++-16 -std=c++23 -Wall -Wextra summary_tests.cpp -o summary_tests-gcc && ./summary_tests-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra summary_tests.cpp -o summary_tests && ./summary_tests
FAIL min, max and mean of 72, 91, 64, 85
FAIL one value is the min, the max and the mean
FAIL the mean keeps its fraction
FAIL the tuple holds min, max and mean, in that order
FAIL the tuple of one value
0 of 5 checks passed
[exit status 1]
$ g++-16 -std=c++23 -Wall -Wextra summary_tests.cpp -o summary_tests-gcc && ./summary_tests-gcc
FAIL min, max and mean of 72, 91, 64, 85
FAIL one value is the min, the max and the mean
FAIL the mean keeps its fraction
FAIL the tuple holds min, max and mean, in that order
FAIL the tuple of one value
0 of 5 checks passed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 summary_session.py

When both functions are right, the tests print 5 of 5 checks passed and the program exits with status 0. Two hints: std::ranges::minmax(values) returns the smallest and the largest value in one pass, as a struct you can unpack with a structured binding, and summarise_tuple can unpack summarise(values) the same way and return a tuple of the three names. Then compare the call sites in the tests: summarise(marks).mean names what it reads, while the tuple needs the reader to remember which position holds the mean.

Interview questions

Fresher · What does auto [a, b] = p; declare, and how is it different from auto& [a, b] = p;?

It declares one unnamed variable and two names. auto [a, b] = p; copies p into the unnamed variable, and a and b refer to the elements of that copy, so changing them leaves p as it was. With auto&, the unnamed variable is a reference to p, so a and b refer to the elements of p itself and assigning to them changes p. const auto& gives read-only access without a copy, which is the usual choice in loops such as for (const auto& [key, value] : m). The number of names must equal the number of elements, and the object must be an array, a tuple-like type such as std::pair or std::tuple, or a class whose non-static data members are all public. A good answer also mentions that a plain auto copies every element of a container it loops over.

Key takeaways

  • std::pair has first and second; std::tuple has any fixed number of elements, read with std::get<i>. A string literal in a deduced pair is a const char*, not a std::string.
  • auto [x, y] = e; declares an unnamed copy of e and names for its elements; auto& and const auto& bind to e itself.
  • Structured bindings unpack arrays, tuple-like types and classes with only public data members, with exactly one name per element.
  • Loop over a map with const auto& [key, value]. A plain auto, or a reference to the wrong pair type, copies every element, and -Wall warns about the second.
  • Tuples compare element by element, so std::tie(b.marks, a.name) < std::tie(a.marks, b.name) sorts by marks from high to low and then by name.
  • Prefer a named struct for results that mean something; C++26 adds _ for unwanted elements and binding packs in templates.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What type does std::pair best{"Asha", 91}; have?

    Choose one answer.

    Show the answer to question 1

    Answer: std::pair<const char*, int>

    The compiler deduces the types from the values, and a string literal decays to const char*. Write std::pair<std::string, int>, or use "Asha"s from std::string_literals, to get a std::string.

  2. Question 2 of 6 A std::map<std::string, Order> has 1,000 elements. How many of them does for (auto [id, order] : orders) copy?

    Type a number.

    Show the answer to question 2

    Answer: 1000 elements

    A plain auto copies each element into the unnamed variable before the names refer to it. Write const auto& [id, order] to read the elements where they are.

  3. Question 3 of 6 Which loop over a std::map<std::string, Marks> m reads every element without copying it?

    Choose one answer.

    Show the answer to question 3

    Answer: for (const auto& [name, marks] : m)

    The map's elements are std::pair<const std::string, Marks>. A reference to a pair with a non-const key does not match that type, so the compiler copies each element into a temporary, and both other loops copy by value.

  4. Question 4 of 6 Which comparison sorts students by marks from high to low, and by name from A to Z when the marks are equal?

    Choose one answer.

    Show the answer to question 4

    Answer: std::tie(b.marks, a.name) < std::tie(a.marks, b.name)

    Putting b's marks on the left reverses the first key, so higher marks come first; keeping a's name on the left keeps the names in ascending order. The second choice sorts both keys ascending, the third both descending.

  5. Question 5 of 6 Which of these can a structured binding unpack in ordinary code outside the class?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • struct Point { int x; int y; };
    • std::pair<std::string, int>
    • int sides[3]

    Arrays, tuple-like types and classes whose data members are all public can be unpacked. A vector's size is known only at run time, and private members cannot be bound outside the class's own members.

  6. Question 6 of 6 Given std::tuple<int, std::string, int> t;, which call does not compile?

    Choose one answer.

    Show the answer to question 6

    Answer: std::get<int>(t)

    Reading a tuple element by its type works only when that type occurs exactly once. int occurs twice here, so the call is ambiguous and is rejected at compile time; the positions 0 and 2 always work.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.