Card 1 / 6
A std::tuple is a fixed-size bundle of values of different types β a struct without field names. Write make_person, which packs (name, age, height) into a std::tuple<std::string, int, double>, and get_age, which reads the age back out by position.
| call | result |
|---|---|
make_person("Ada", 36, 1.7) |
a tuple with element 0 "Ada", element 1 36, element 2 1.7 |
get_age(make_person("Ada", 36, 1.7)) |
36 |
get_age(make_person("Bob", 0, 0.5)) |
0 |
return std::make_tuple(std::move(name), age, height); deduces the element types from the arguments (a braced return {std::move(name), age, height}; also works because the return type is spelled out). Reading uses a compile-time index: std::get<1>(person) β positions are 0-based and there is no run-time bounds check because the index is a template argument. When a type appears only once in the tuple, std::get<int>(person) selects it by type instead.
// 1) Bundle (name, age, height) into a std::tuple.
std::tuple<std::string, int, double> make_person(std::string name, int age, double height) {
return std::make_tuple(std::move(name), age, height); // deduces tuple<string, int, double>
}
// 2) Read the age back out. Tuple elements are addressed by POSITION,
// fixed at compile time: element 0 is the name, 1 the age, 2 the height.
int get_age(const std::tuple<std::string, int, double>& person) {
return std::get<1>(person); // std::get<Index>: 0-based, checked by the compiler
}Card 2 / 6
A C++17 structured binding unpacks a pair, tuple, array or plain struct into named variables in one line: auto [a, b] = pair;. Its most common home is a range-for over a map. Write top_scorer: given a map from name to score, return the name whose score is the highest, iterating with for (const auto& [name, score] : scores) rather than .first / .second.
scores |
result | why |
|---|---|---|
{{"ada", 90}, {"bob", 97}, {"cy", 95}} |
"bob" |
97 is the highest score |
{{"zed", 50}, {"amy", 50}} |
"amy" |
tie β keep the first in map order (alphabetical) |
{{"a", -5}, {"b", -2}} |
"b" |
negatives are fine: β2 is the larger |
{} |
"" |
empty map |
Track the best seen so far and let a strict > decide:
std::string best_name; // "" until something beats best_score
int best_score = std::numeric_limits<int>::min(); // lower than any real score
for (const auto& [name, score] : scores) { // each pair unpacked by position: key, value
if (score > best_score) { // strict '>' keeps the earliest of equal scores
best_name = name;
best_score = score;
}
}
return best_name;const auto& binds each std::pair<const std::string, int> by reference, so no name is copied; name and score are just aliases for .first and .second.
// Return the name with the HIGHEST score, or "" for an empty map. On a tie,
// keep the FIRST winner in map order (alphabetical). Iterate with a C++17
// structured binding β `for (const auto& [name, score] : scores)` β instead
// of touching `.first` / `.second`.
std::string top_scorer(const std::map<std::string, int>& scores) {
std::string best_name; // "" until something beats best_score
int best_score = std::numeric_limits<int>::min(); // lower than any real score
for (const auto& [name, score] : scores) { // each pair unpacked by position: key, value
if (score > best_score) { // strict '>' keeps the earliest of equal scores
best_name = name;
best_score = score;
}
}
return best_name;
}Card 3 / 6
std::tie(a, b, c) builds a tuple of references to existing variables, and tuples compare lexicographically β first element decides, then the second, and so on. Write before(a, b) for the Person struct (declared above the editable region: last, first, age): true when a sorts before b by last name, then first name, then age.
a |
b |
result | decided by |
|---|---|---|---|
Adams, Zoe, 50 |
Baker, Al, 20 |
true |
last name |
Lee, Amy, 40 |
Lee, Bob, 30 |
true |
first name |
Lee, Amy, 30 |
Lee, Amy, 40 |
true |
age |
Lee, Amy, 30 |
Lee, Amy, 30 |
false |
equal β neither is before |
return std::tie(a.last, a.first, a.age) < std::tie(b.last, b.first, b.age); β one line, no copies (references only), no nested if/else. The other classic use of std::tie is receiving a tuple into variables that already exist: std::tie(q, r) = divmod(17, 5);. In C++20 the same comparison is one operator<=> = default; on a C++17 compiler std::tie is the idiom.
// Return true if `a` sorts before `b`: by last name, then first name, then
// age (all ascending). std::tie makes a tuple of REFERENCES to the fields,
// and tuples compare lexicographically β so one line replaces a chain of
// if/else-if comparisons.
bool before(const Person& a, const Person& b) {
return std::tie(a.last, a.first, a.age) < std::tie(b.last, b.first, b.age);
}Card 4 / 6
std::optional<T> holds either a T or nothing (std::nullopt) β the honest return type for "might not exist", replacing sentinels like -1. Write find_index: the position of the first element equal to target, or an empty optional.
v |
target |
result |
|---|---|---|
{4, 8, 15, 16} |
15 |
2 |
{7, 7, 7} |
7 |
0 β first match |
{1, 2, 3} |
9 |
empty (std::nullopt) |
{} |
1 |
empty |
Return the index as soon as you see a match; after the loop, return "nothing":
for (std::size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) return i; // a size_t converts to optional<size_t> implicitly
}
return std::nullopt; // the empty state β the whole point of optionalOn the caller's side, an std::optional<std::size_t> r offers:
if (r) or r.has_value() β is there a value?*r β read it, unchecked (undefined behaviour if empty)r.value() β read it, checked: throws std::bad_optional_access if emptyr.value_or(999) β the value, or the fallback, in one expression// Return the index of the first element equal to `target`, or an EMPTY
// optional if it is absent. std::optional<T> either holds a T or holds
// nothing (std::nullopt) β no sentinel like -1 or SIZE_MAX needed.
std::optional<std::size_t> find_index(const std::vector<int>& v, int target) {
for (std::size_t i = 0; i < v.size(); ++i) {
if (v[i] == target) return i; // implicit conversion: size_t -> optional<size_t>
}
return std::nullopt; // "no value" β the point of optional
}Card 5 / 6
std::variant<Circle, Rect> holds exactly one of its alternatives at a time β a type-safe union. std::visit(visitor, v) calls visitor(circle) or visitor(rect) depending on which is active, and refuses to compile if the visitor cannot handle every alternative. Complete AreaVisitor and area for the Circle / Rect shapes declared above the editable region.
shape holds |
area(shape) |
|---|---|
Circle{1.0} |
3.14159265 |
Circle{2.0} |
12.5663706 |
Rect{3.0, 4.0} |
12.0 |
Rect{0.0, 4.0} |
0.0 |
The visitor is a struct with one operator() per alternative; std::visit picks the one matching the active type:
struct AreaVisitor {
double operator()(const Circle& c) const { return kPi * c.radius * c.radius; }
double operator()(const Rect& r) const { return r.width * r.height; }
};
double area(const Shape& shape) {
return std::visit(AreaVisitor{}, shape); // calls the operator() for whatever shape holds
}Other tools you will meet on a variant v:
std::holds_alternative<Rect>(v) β is Rect the active type?std::get<Rect>(v) β extract it; throws std::bad_variant_access if another type is activestd::get_if<Rect>(&v) β a pointer to it, or nullptrv.index() β position of the active alternative (Circle is 0, Rect is 1)// std::visit(visitor, shape) calls visitor(circle) or visitor(rect) depending
// on which alternative the variant currently holds. The visitor needs one
// operator() per alternative β forget one and the code does not compile.
struct AreaVisitor {
double operator()(const Circle& c) const {
return kPi * c.radius * c.radius;
}
double operator()(const Rect& r) const {
return r.width * r.height;
}
};
// Return the area of whatever `shape` holds.
double area(const Shape& shape) {
return std::visit(AreaVisitor{}, shape); // dispatches on the active alternative
}Card 6 / 6
std::string_view is a non-owning (pointer, length) window onto characters that live somewhere else β a std::string, a literal, a buffer. Shrinking a view moves the window; it never copies or allocates. Write trim, which returns s without leading and trailing spaces, as a view into the same characters.
s |
result |
|---|---|
" hello " |
"hello" |
" x" |
"x" |
"a b" |
"a b" β interior space is kept |
" " |
"" |
"" |
"" |
while (!s.empty() && s.front() == ' ') s.remove_prefix(1); then the same with back() and remove_suffix(1); return s;. The !s.empty() guard matters: front() on an empty view is undefined. Because the result still points into the caller's data, it is only valid while that data lives β never return a string_view into a local std::string.
// Return `s` without leading and trailing spaces. std::string_view is a
// non-owning (pointer, length) view over characters that live elsewhere:
// shrinking it never copies or allocates. Do NOT build a std::string.
std::string_view trim(std::string_view s) {
while (!s.empty() && s.front() == ' ') s.remove_prefix(1); // slide the start forward
while (!s.empty() && s.back() == ' ') s.remove_suffix(1); // pull the end back
return s; // still points into the caller's data
}