Structured bindings are a C++17 feature that allows you to bind multiple variables to the elements of a structured object, such as a tuple or struct, in a single declaration. This can make your code more concise and easier to read, especially when working with complex data structures. In this blog post, we will look at the basic syntax of this cool feature, its use cases, and even some real-life code examples.

The basics of structured binding  

Starting from C++17, you can write:

std::set<int> mySet;
auto [iter, inserted] = mySet.insert(10);

insert() returns std::pair indicating if the element was inserted or not, and the iterator to this element. Instead of pair.first and pair.second, you can use variables with concrete names.

(You can also assign the result to your variables by using std::tie(); still, this technique is not as convenient as structured bindings in C++17)

Such syntax is called a structured binding expression.

The Syntax  

The basic syntax for structured bindings is as follows:

auto [a, b, c, ...] = expression;
auto [a, b, c, ...] { expression };
auto [a, b, c, ...] ( expression );

The compiler introduces all identifiers from the a, b, c, ... list as names in the surrounding scope and binds them to sub-objects or elements of the object denoted by expression.

Behind the scenes, the compiler might generate the following pseudo code:

auto tempTuple = expression;
using a = tempTuple.first;
using b = tempTuple.second;
using c = tempTuple.third;

Conceptually, the expression is copied into a tuple-like object (tempTuple) with member variables that are exposed through a, b and c. However, the variables a, b, and c are not references; they are aliases (or bindings) to the generated object member variables. The temporary object has a unique name assigned by the compiler.

For example:

std::pair a(0, 1.0f);
auto [x, y] = a;

x binds to int stored in the generated object that is a copy of a. And similarly, y binds to float.

Modifiers  

Several modifiers can be used with structured bindings (similarly like on auto):

const modifiers:

const auto [a, b, c, ...] = expression;

References:

auto& [a, b, c, ...] = expression;
auto&& [a, b, c, ...] = expression;

For example:

std::pair a(0, 1.0f);
auto& [x, y] = a;
x = 10;  // write access
// a.first is now 10

In the example, x binds to the element in the generated object, which is a reference to a.

Now it’s also relatively easy to get a reference to a tuple member:

auto& [ refA, refB, refC, refD ] = myTuple;

Or better via a const reference:

const auto& [ refA, refB, refC, refD ] = myTuple;

You can also add [[attribute]] to structured bindings:

[[maybe_unused]] auto& [a, b, c, ...] = expression;

Binding  

Structured Binding is not only limited to tuples; we have three cases from which we can bind from:

1. If the initializer is an array:

// works with arrays:
double myArray[3] = { 1.0, 2.0, 3.0 };  
auto [a, b, c] = myArray;

In this case, an array is copied into a temporary object, and a, b, and c refers to copied elements from the array.

The number of identifiers must match the number of elements in the array.

2. If the initializer supports std::tuple_size<>, provides get<N>() and also exposes std::tuple_element functions:

std::pair myPair(0, 1.0f);
auto [a, b] = myPair; // binds myPair.first/second

In the above snippet, we bind to myPair. But this also means you can provide support for your classes, assuming you add the get<N> interface implementation. See an example in the later section.

3. If the initializer’s type contains only non-static data members:

struct Point  { 
    double x; 
    double y; 
};

Point GetStartPoint() {
    return { 0.0, 0.0 };
}
    
const auto [x, y] = GetStartPoint();

x and y refer to Point::x and Point::y from the Point structure.

The class doesn’t have to be POD, but the number of identifiers must equal to the number of non-static data members. The members must also be accessible from the given context.

C++17/C++20 changes  

During the work on C++20, there were several proposals that improved the initial support for structured bindings:

  • P0961 - Relaxing the structured bindings customization point finding rules
  • P0969 - Allow structured bindings to accessible members
  • P1091 - Extending structured bindings to be more like variable declarations
  • P1381 - Reference capture of structured bindings

It looks like GCC implemented those features working even for the C++17 mode.

For example, you can even capture them in lambda:

std::pair xy { 42.3, 100.1 };
auto [x, y] = xy;
auto foo = [&x, &y]() {
    std::cout << std::format("{}, {}", x, y);
};
foo();

See at Compiler Explorer

Iterating through maps  

If you have a std::map of elements, you might know that internally, they are stored as pairs of <const Key, ValueType>.

Now, when you iterate through elements of that map:

for (const auto& elem : myMap) { ... }

You need to write elem.first and elem.second to refer to the key and value. One of the coolest use cases of structured binding is that we can use it inside a range based for loop:

std::map<KeyType, ValueType> myMap;    
// C++14:
for (const auto& elem : myMap) {  
    // elem.first - is velu key
    // elem.second - is the value
} 
// C++17:
for (const auto& [key,val] : myMap) {  
    // use key/value directly
} 

In the above example, we bind to a pair of [key, val] so we can use those names in the loop. Before C++17, you had to operate on an iterator from the map - which returns a pair <first, second>. Using the real names key/value is more expressive.

The above technique can be used in the following example:

#include <map>
#include <iostream>

int main() {
    const std::map<std::string, int> mapCityPopulation {
        { "Beijing", 21'707'000 },
        { "London", 8'787'892 },
        { "New York", 8'622'698 }
    };
    
    for (const auto&[city, population] : mapCityPopulation)
        std::cout << city << ": " << population << '\n';
}

Run @Compiler Explorer

In the loop body, you can safely use the city and population variables.

Working with structures and arrays  

Here’s another example where structured binding might be handy:

#include <iostream>
#include <format>
#include <array>

struct Color {
    // internal representation...
    int data { 0 };
    int model { 0 };

    std::array<unsigned char, 3> getRGB() const {
        // do some conversion from the internal model...
        return {255, 128, 255 };
    }
    std::array<double, 3> getHSV() const {
        // do some conversion from the internal model...
        return {0.5, 0.1, 1. };
    }
};

int main() {
    Color col{};
    auto [r, g, b] = col.getRGB();
    std::cout << std::format("{}, {}, {}\n", r, g, b);
    auto [h, s, v] = col.getHSV();
    std::cout << std::format("{}, {}, {}\n", h, s, v);
}

Run @Compiler Explorer

As you can see, the code has a Color structure that might contain some internal representation, and you use getters to get RGB or HSV models. Thanks to the structured binding, it’s easy to name those sub-objects, rather than rely on arrays.

Real code  

Let’s have a look at some code in the wild. I found the following use cases:

In Tensorflow: nccl_collective_thunk.cc

auto [_, was_inserted] =
      done_events_.insert({device_ordinal, std::move(done_event)});

Powertoys Microsoft: updating.cpp

auto [installer_download_url, installer_filename] = extract_installer_asset_download_info(release_object);
co_return new_version_download_info{ extract_release_page_url(release_object),
                                                 std::move(github_version),
                                                 std::move(installer_download_url),
                                                 std::move(installer_filename) };

Microsoft Terminal: state.cpp

const auto [colorForeground, colorBackground] = renderSettings.GetAttributeColors(textAttributes);

std::from_chars() and bitcoin: bitcoin

const auto [first_nonmatching, error_condition] 
    = std::from_chars(val.data(), val.data() + val.size(), result);

Back to you

  • Do you use structured bindings?
  • In what use case you have tried them?

Share your feedback in the comments below.