C++17 comes with a new feature, which combines syntactic sugar and automatic type deduction: structured bindings. These help to assign values from pairs, tuples, and structs into individual variables. In other programming languages, this is also called unpacking.
Using structured bindings to unpack bundled return values
How to do it...
Applying a structured binding in order to assign multiple variables from one bundled structure is always one step. Let's first see how it was done before C++17. Then, we can have a look at multiple examples that show how we can do it in C++17:
- Accessing individual values of an std::pair: Imagine we have a mathematical function, divide_remainder, which accepts a dividend and a divisor parameter and returns the fraction of both as well as the remainder. It returns those values using an std::pair bundle:
std::pair<int, int> divide_remainder(int dividend, int divisor);
Consider the following way of accessing the individual values of the resulting pair:
const auto result (divide_remainder(16, 3));
std::cout << "16 / 3 is "
<< result.first << " with a remainder of "
<< result.second << '\n';
Instead of doing it as shown in the preceding code snippet, we can now assign the individual values to individual variables with expressive names, which is much better to read:
auto [fraction, remainder] = divide_remainder(16, 3);
std::cout << "16 / 3 is "
<< fraction << " with a remainder of "
<< remainder << '\n';
- Structured bindings also work with std::tuple: Let's take the following example function, which gets us online stock information:
std::tuple<std::string,
std::chrono::system_clock::time_point, unsigned>
stock_info(const std::string &name);
Assigning its result to individual variables looks just like in the example before:
const auto [name, valid_time, price] = stock_info("INTC");
- Structured bindings also work with custom structures: Let's assume a structure like the following:
struct employee {
unsigned id;
std::string name;
std::string role;
unsigned salary;
};
Now, we can access these members using structured bindings. We can even do that in a loop, assuming we have a whole vector of those:
int main()
{
std::vector<employee> employees {
/* Initialized from somewhere */};
for (const auto &[id, name, role, salary] : employees) {
std::cout << "Name: " << name
<< "Role: " << role
<< "Salary: " << salary << '\n';
}
}
How it works...
Structured bindings are always applied with the same pattern:
auto [var1, var2, ...] = <pair, tuple, struct, or array expression>;
- The list of variables var1, var2, ... must exactly match the number of variables contained by the expression being assigned from.
- The <pair, tuple, struct, or array expression> must be one of the following:
- An std::pair.
- An std::tuple.
- A struct. All members must be non-static and defined in the same base class. The first declared member is assigned to the first variable, the second member to the second variable, and so on.
- An array of fixed size.
- The type can be auto, const auto, const auto&, and even auto&&.
If we write too many or not enough variables between the square brackets, the compiler will error out, telling us about our mistake:
std::tuple<int, float, long> tup {1, 2.0, 3};
auto [a, b] = tup; // Does not work
This example obviously tries to stuff a tuple variable with three members into only two variables. The compiler immediately chokes on this and tells us about our mistake:
error: type 'std::tuple<int, float, long>' decomposes into 3 elements, but only 2 names were provided
auto [a, b] = tup;
There's more...
A lot of fundamental data structures from the STL are immediately accessible using structured bindings without us having to change anything. Consider, for example, a loop that prints all the items of an std::map:
std::map<std::string, size_t> animal_population {
{"humans", 7000000000},
{"chickens", 17863376000},
{"camels", 24246291},
{"sheep", 1086881528},
/* … */
};
for (const auto &[species, count] : animal_population) {
std::cout << "There are " << count << " " << species
<< " on this planet.\n";
}
This particular example works because when we iterate over an std::map container, we get the std::pair<const key_type, value_type> nodes on every iteration step. Exactly these nodes are unpacked using the structured bindings feature (key_type is the species string and value_type is the population count size_t) in order to access them individually in the loop body.
Before C++17, it was possible to achieve a similar effect using std::tie:
int remainder;
std::tie(std::ignore, remainder) = divide_remainder(16, 5);
std::cout << "16 % 5 is " << remainder << '\n';
This example shows how to unpack the resulting pair into two variables. The std::tie is less powerful than structured bindings in the sense that we have to define all the variables we want to bind to before. On the other hand, this example shows a strength of std::tie that structured bindings do not have: the value std::ignore acts as a dummy variable. The fraction part of the result is assigned to it, which leads to that value being dropped because we do not need it in that example.
Back in the past, the divide_remainder function could have been implemented in the following way, using output parameters:
bool divide_remainder(int dividend, int divisor,
int &fraction, int &remainder);
Accessing it would have looked like the following:
Â
int fraction, remainder;
const bool success {divide_remainder(16, 3, fraction, remainder)};
if (success) {
std::cout << "16 / 3 is " << fraction << " with a remainder of "
<< remainder << '\n';
}
A lot of people will still prefer this over returning complex structures like pairs, tuples, and structs, arguing that this way the code would be faster, due to avoided intermediate copies of those values. This is not true any longer for modern compilers, which optimize intermediate copies away.