@[#Structured Bindings]
#Structured Bindings
Structured Bindings allow us to define several objects in one go, in a more natural way than in the previous versions of C++.
From C++11 to C++17
This concept is not new in itself. Previously, it was always possible to return multiple values from a function and access them using std::tie.
Consider the function:
std::tuple<char, int, bool> mytuple()
{
char a = ‘a’;
int i = 123;
bool b = true;
return std::make_tuple(a, i, b);
}
std::tuple<char, int, bool> mytuple()
{
char a = ‘a’;
int i = 123;
bool b = true;
return std::make_tuple(a, i, b);
}
This returns three variables all of different types. To access these from a calling function prior to C++17, we would need something like:
char a;
int i;
bool b;
std::tie(a, i, b) = mytuple();
char a;
int i;
bool b;
std::tie(a, i, b) = mytuple();
Where the variables have to be defined before use and the types known in advance.
But using Structured Bindings, we can simply do this as:
auto [a, i, b] = mytuple();
1
auto [a, i, b] = mytuple();
which is a much nicer syntax and is also consistent with modern C++ style using auto almost whenever possible.
So what can be used with a Structured Binding initialization? Basically anything that is a compound type – struct, pair and tuple. Let’s see several cases where it can be useful.
Returning compound objects
This is the easy way to assign the individual parts of a compound type (such as a struct, pair etc) to different variables all in one go – and have the correct types automatically assigned. So let’s have a look at an example. If we insert into a map, then the result is a std::pair:
std::map<char,int> mymap;
auto mapret = mymap.insert(std::pair(‘a’, 100));
std::map<char,int> mymap;
auto mapret = mymap.insert(std::pair(‘a’, 100));
And if anyone is wondering why the types are not explicitly stated for pair, then the answer is Template Argument Deduction in C++17 – keep reading!
So to determine if the insert was successful or not, we could extract the info from what the insert method returned:
The problem with this code is that a reader needs to look up what .second is supposed to mean, if only mentally. But using Structured Bindings, this becomes:
auto [itelem, success] = mymap.insert(std::pair(’a’, 100));
If (!success) {
// Insert failure
}
auto [itelem, success] = mymap.insert(std::pair(’a’, 100));
If (!success) {
// Insert failure
}
Where itelem is the iterator to the element and success is of type bool, with true for insertion success. The types of the variables are automatically deduced from the assignment – which is much more meaningful when reading code.
As a sneak peek into the last section, as C++17 now has Selection Initialization, then we could (and probably would) write this as:
if (auto [itelem, success] = mymap.insert(std::pair(‘a’, 100)); success) {
// Insert success
}
if (auto [itelem, success] = mymap.insert(std::pair(‘a’, 100)); success) {
// Insert success
}
But more on this in a moment.
Iterating over a compound collection
Structured Bindings also work with range-for as well. So considering the previous mymap definition, prior to C++17 we would iterate it with code looking like this:
for (const auto& entry : mymap) {
// Process key as entry.first
// Process value as entry.second
}
for (const auto& entry : mymap) {
// Process key as entry.first
// Process value as entry.second
}
Or maybe, to be more explicit:
for (const auto& entry : mymap) {
auto& key = entry.first;
auto& value = entry.second;
// Process entry
}
for (const auto& entry : mymap) {
auto& key = entry.first;
auto& value = entry.second;
// Process entry
}
But Structured Bindings allow us to write it more directly:
for (const auto&[key, value] : mymap) {
// Process entry using key and value
}
for (const auto&[key, value] : mymap) {
// Process entry using key and value
}
The usage of the variables key and value are more instructive than entry.first and entry.second – and without requiring the extra variable definitions.
Direct initialization
But as Structured Bindings can initialize from a tuple, pair etc, can we do direct initialization this way?
Yes we can. Consider:
auto a = ‘a’;
auto i = 123;
auto b = true;
auto a = ‘a’;
auto i = 123;
auto b = true;
which defines variables a as type char with initial value ‘a’, i as type int with initial value 123 and b as type bool with initial value true.
Using Structured Bindings, this can be written as:
auto [a, i, b] = tuple(‘a’, 123, true); // With no types needed for the tuple!
1
auto [a, i, b] = tuple(‘a’, 123, true); // With no types needed for the tuple!
This will define the variables a, i, b the same as if the separate defines above had been used.
Is this really an improvement over the previous definition? OK, we’ve done in one line what would have taken three but why would we want to do this?
Consider the following code:
{
istringstream iss(head);
for (string name; getline(iss, name); )
// Process name
}
{
istringstream iss(head);
for (string name; getline(iss, name); )
// Process name
}
Both iss and name are only used within the for block, yet iss has to be declared outside of the for statement and within its own block so that the scope is limited to that required.
This is weird, because iss belongs to the for loop.
Initialization of multiple variables of the same type has always been possible. For example:
for (int i = 0, j = 100; i < 42; ++i, --j) {
// Use i and j
}
for (int i = 0, j = 100; i < 42; ++i, --j) {
// Use i and j
}
But what we’d like to write – but can’t – is:
for (int i = 0, char ch = ‘ ‘; i < 42; ++i) { // Does not compile
// Use i and ch
}
for (int i = 0, char ch = ‘ ‘; i < 42; ++i) { // Does not compile
// Use i and ch
}
With Structured Bindings we can write:
for (auto[iss, name] = pair(istringstream(head), string {}); getline(iss, name); ) {
// Process name
}
for (auto[iss, name] = pair(istringstream(head), string {}); getline(iss, name); ) {
// Process name
}
and
for (auto[i, ch] = pair(0U, ‘ ‘); i < 42; ++i) { // The 0U makes i an unsigned int
// Use i and ch
}
for (auto[i, ch] = pair(0U, ‘ ‘); i < 42; ++i) { // The 0U makes i an unsigned int
// Use i and ch
}
Which allows the variables iss and name (and i and ch) to be defined within the scope of the for statement as needed and also their type to be automatically determined.
And likewise with the if and switch statements, which now take optional Selection Initialization in C++17 (see below). For example:
if (auto [a, b] = myfunc(); a < b) {
// Process using a and b
}
if (auto [a, b] = myfunc(); a < b) {
// Process using a and b
}
Note that we can’t do everything with structured bindings, and trying to fit them in to every situation can make the code more convoluted. Consider the following example:
if (auto [box, bit] = std::pair(std::stoul§, boxes.begin()); (bit = boxes.find(box)) != boxes.end()){
// Process if using both box and bit variables
}
1
2
3
if (auto [box, bit] = std::pair(std::stoul§, boxes.begin()); (bit = boxes.find(box)) != boxes.end()){
// Process if using both box and bit variables
}
Here variable box is defined as type unsigned long and has an initial value returned from stoul§. stoul(), for those not familiar with it, is a function which takes a type std::string as its first argument (there are other optional ones – including base) and parses its content as an integral number of the specified base (defaults to 10), which is returned as an unsigned long value.
The type of variable bit is that of an iterator for boxes and has an initial value of .begin() – which is just to determine its type for auto. The actual value of variable bit is set in the condition test part of the if statement. This highlights a constraint with using Structured Bindings in this way. What we really want to write is:
if (const auto [box, bit] = std::pair(std::stoul§, boxes.find(box)); bit != boxes.end()){
// This doesn’t compile
// Process if using both box and bit variables
}
if (const auto [box, bit] = std::pair(std::stoul§, boxes.find(box)); bit != boxes.end()){
// This doesn’t compile
// Process if using both box and bit variables
}
But we can’t because a variable declared within an auto type specifier cannot appear within its own initializer! Which is kind of understandable.
So to sum up, the advantages of using Structured Bindings are:
a single declaration that declares one or more local variables
that can have different types
whose types are always deduced using a single auto
assigned from a composite type.
The drawback, of course, is that an intermediary (eg std::pair) is used. This needn’t necessarily impact upon performance (it is only done once at the start of the loop anyhow) as move semantics would be used where possible – but note that where a type used is non-moveable (eg like std::array) then this could incur a performance ‘hit’ depending upon what the copy operation involved.
But don’t pre-judge the compiler and pre-optimize code! If the performance isn’t as required, then use a profiler to find the bottleneck(s) – otherwise you are wasting development time. Just write the simplest / cleanest code that you can.