C++ 17: Structured Binding - From Zero to Hero

Structured binding allows to initialise multiple entities by members of another object, for instance:

struct stb_node
{
	int index = 0;
	string value;
};

stb_node node1{1, "First"};
cout << node1.index << ", " << node1.value << endl;

auto [u, v] = node1;
cout << u << ", " << v << endl;

Produces output:

1, First
1, First

When you break in a debugger, it shows that there are two local variables int u and string v created. u and v are called structural bindings. The purpose of structural bindings is to make code more readable by binding the value directly to names.

This becomes more apparent when working with maps:

map<int, string> m = ...;

// pre - C++17 way
for (const auto& el : m)
{
	cout << el.first << ", " << el.second << endl;
}

// C++17 way
for (const auto& [index, value] : m)
{
	cout << index << ", " << value << endl;
}

In the second syntax option, the code is carrying the context that it’s working with index and value, unlike el.first and el.second which doesn’t make sense. Of course you can create references to key and value i.e. auto& index = el.first (and that’s what I often do to improve readability) but that makes code more verbose.

Note that decltype(index) is int and decltype(value) is string - there is no special magic types involved, these are just other names for struct members.

Also, as with normal assignment, auto [u, v] = node1 creates a copy of struct members, with all the consequences i.e. modifying u modifies a copy of u.

Qualifiers

To avoid copying and enforce constant expressions, you can use qualifiers, for instance:

const auto [u1, v1] = node1;
u1 = 1;

fails on line 2 when trying to modify a copy of node1.index.

const auto& [u2, v2] = node1;
u2 = 2;

fails on line 2 when trying to modify a reference of node1.index.

More Usages

In general, structured bindings can be used with:

  1. Structs with public data members as above.
  2. Raw C-style arrays.
  3. “Tuple-like” objects.

For structs or classes, the decomposed members must be members of the same class definition. Inheritance is not supported. So this won’t work:

struct B {
	int a = 1;
	int b = 2;
};

struct C : B {
    int c = 3;
}

auto [i, j, k] = C{};	// won't work, gives an error "type "C" has no components to bind to"

For arrays, binding only works for arrays of known size:

int arr[] = { 1, 2 };
auto [e1, e2] = arr;	// OK
auto [e1] = arr;	    // won't compile, gives an error "there are more elements than there are binding names"

std::tuple decomposes nicely i.e.

std::tuple<char,float,std::string> do_something();
// ...
auto [a,b,c] = do_something();

std::pair is just like before, and is logically a subset of tuple.

Redefining Values

Looking at the example before, how do you call do_someting again and assign to a, b, c? You can’t. However, if the types match, use std::tie:

std::tuple<char, float, std::string> do_something();
// ...
auto [a, b, c] = do_something();
// redefine:
std::tie(a, b, c) = do_something();

Of course you could just introduce new set of variables, however you wouldn’t be able to do that in a loop, like:

std::boyer_moore_searcher bm{sub.begin(), sub.end()};
for (auto [beg, end] = bm(text.begin(), text.end()); beg != text.end(); std::tie(beg,end) = bm(end, text.end())) {
	// ...
}

Thanks! You can always email me or use contact form for more questions/comments etc.