C++17: Structured bindings

When accessing to an element of a given compound type, you probably want to get its internal fields in order to work with them.

When tuples were introduced (my post about tuples), the way of doing this was similar to this:

std::tuple<int, std::string, std::string> person { 20050316, "Isabel", "Pantoja" };
int birth_date = std::get<0>(person);
std::string& first_name = std::get<1>(person);
std::string& last_name = std::get<2>(person);

In this way, you were able to access any elements inside the tuple. Quite useful, but quite verbose as well.

So, std::tie could help us to make this code by far easier to read:

std::tuple<int, std::string, std::string> person { 20050316, "Isabel", "Pantoja" };

int birth_date;
std::string first_name;
std::string last_name;

std::tie(birth_date, first_name, last_name) = person;

The magic that std::tie does here is extracting all elements inside of the tuple and mapping them to the values passed as non-const-references to the std::tie function.

C++17 introduced “structured binding”, a far better and evident and elegant way of doing this without getting lost in what std::tie or std::get<N> do.

Structured binding, as its name suggests, binds a set of variables to its corresponding values inside a tuple. Actually these constructs are supported:

  • std::tuple<T...>
  • std::pair<T, U>
  • std::array<T, N> and stack-based old-style arrays
  • Structs!

How does it work?

The example above could be re-written like this:

std::tuple<int, std::string, std::string> person { 20050316, "Isabel", "Pantoja" };
auto& [birth_date, first_name, last_name] = person;

So, the compiler will “decompose” person in its “atomic” values and will bind the birth_date reference to the first value in person, the first_name to the second value and the last_name to the third one.

Notice that birth_date, first_name, and last_name are references because of auto&. If I would have used auto instead, they would have been actual values instead.

And you can do the same thing with arrays:

std::array<int, 4> values {{ 8, 5, 2, 9 }};
auto [v0, v1, v2, v3] = values;
std::cout << "Sum: " << (v0 + v1 + v2 + v3) << "\n";

int values2[] = { 5, 4, 3 };
auto [i0, i1, i2] = value2;
std::cout << i0 << "; " << i1 << "; " << i2 << "\n";

With std::pair:

auto pair = std::make_pair(19451019, "María Zambrana");
auto& [date, name] = pair;
std::cout << date << ": " << name << "\n";

Or even with structs!!!

struct Point
{
  int x;
  int y;
};

Point p { 10, 25 };
auto [x, y] = p;
std::cout << "(" << x << "; " << y << ")\n";

This feature makes iterating maps incredibly elegant.

Compare this pre-C++11 code:

for (std::map<std::string, std::string>::const_iterator it = translations.begin();
     it != translations.end();
     it++)
{
  std::cout << "English: " << it->first << "; Spanish: " << it->second << "\n";
}

Using C++17, it could have been written like this in C++17:

for (auto& [english, spanish] : translations)
{
  std::cout << "English: " << english << "; Spanish: " << spanish << "\n";
}

Amazing!

Advertisement

C++: boost::pfr

At work I used this library some days ago and I find it amazingly useful:

Basically, boost::pfr lets you work with the fields of a given struct, as a std::tuple, letting you access the elements through an index representing the position where the field is defined at inside the struct.

boost::pfr has also the benefit of being header-file-only and of not having any dependency with any other boost library, making it easy to install (copying its header files and that’s it) and use it.

Let’s see the example below. Suppose you have a struct Person:

struct Person
{
  std::string first_name;
  std::string last_name;
  int birth_year;
};

With boost::pfr, you can access any member in the struct given an index using a method called get() that is very similar to its std::tuple counterpart:

#include "boost/pfr.hpp"

int main()
{
  Person composer { "Martin", "Gore", 1961};
  
  auto& a = boost::pfr::get<0>(composer); // will return a std::string reference
  auto& b = boost::pfr::get<1>(composer);
  auto& c = boost::pfr::get<2>(composer); // will return a reference to the int field

  std::cout << a << ", " << b << ", " << c << "\n";
  return 0;
}

To get then number of elements in a struct, you have boost::pfr::tuple_size:

int main()
{
  std::cout << boost::pfr::tuple_size<Person>::value << "\n";  // returns 3 for our struct
}

To get the type of a given parameter, you have boost::pfr::tuple_element:

int main()
{
  boost::pfr::tuple_element<0, Person>::type s; 
  return 0;
}

In the example above, I declared a variable s whose type is the same type of the element in the 0-th position in the struct Person (i.e. a std::string).

Use cases:

You could create a generic library to iterate through all the members of any struct and do something with each member (kind of std::for_each [boost::pfr provides a for_each_field function template to do this iteration] or std::visit), for example, printing its values out (see my example below), saving them to disk, deserializing them or populating its values from a library (think of a SQL result set or a JSON object).

On printing a struct values out, boost::pfr already ships with a method called io_fields that does exactly that:

int main()
{
  Person singer { "Dave", "Gahan", 1962 };
  std::cout << boost::pfr::io_fields(singer) << "\n";
}

This prints out “{ Dave, Gahan, 1962 }“.

boost::pfr also ships with a lot of utilities to compare elements from a struct in other or to use any struct as a key on standard library maps and sets.

To learn more about this nice library, visit its documentation: https://apolukhin.github.io/pfr_non_boost/