C++17 introduces a nifty feature called structured binding, a new syntax for unpacking elements from arrays, tuples, structs and classes, which allows us to write extremely Python-like code

Pre-C++17, extracting all fields from a tuple was a two step process:

auto t = std::make_tuple(1.0, "2", '3');
float f;
const char* i;
char c;
std::tie(f, i, c) = t; // f = 1.0, i = "2", c = '3'

Structured binding makes the declaration and assignment of elements a one step process:

auto t = std::make_tuple(1.0, "2", '3');
auto [f, i, c] = t; // f = 1.0, i = "2", c = '3'

almost the same as is done in Python:

a = (1,2,3)
x, y, z = a # x = 1, y = 2, z = 3

Structured binding can also be extended to arrays:

int arr[] = {1, 2, 3};
auto [i,j,k] = arr;

or to unpacking public members of structs and classes:

class C
{
public:
	int i;
	int j;
	C(int i, int j)
		: i(i), j(j)
	{}
};
C c(3,4);
auto [a,b] = c; // a = 3, b = 4

or used in range-for loops:

std::map<int, std::string> m = { {1, "One"} ,  {2, "Two"}, {3, "Three"} };
for (auto [key, value] : m)
{
	// Use key/value here
	std::cout<<key<<":"<<value<<std::endl;
}

One advantage structured binding has over std::tie is that we can very easily obtain references to tuple members, whereas C++14 requires us to use get() on individual members to obtain references. For instance:

std::tuple t = std::make_tuple(1, 2, 3, 4);

// C++14
int& first = std::get<0>(t);
int& second = std::get<1>(t);
//...

whereas in C++17, we can simply have

// C++17
auto& [first, second, third, forth] = t;

auto const& [const_first, const_second, const_third, const_forth] = t;

It is also possible for us to add support for structured binding to classes by specializing std::get, std::tuple_size and std::tuple_element for that class 1

For instance, consider class C

class C
{
    int a;
    char b;
    public:
    C(int a, char b)
        : a(a), b(b)
    {}
};

First we define get<N>(), which returns the value for the nth object in our class.

This can be a member function or an external function, as shown below

// Get nth element
template<size_t n> decltype(auto) get(const C&);

template<> decltype(auto) get<0>(const C& c)
{   
    return c.a;
}

template<> decltype(auto) get<1>(const C& c)
{
    return c.b;
}

Next, we define a specialization of std::tuple_size for our class, which returns the number of elements in the class which can be unpacked

namespace std
{   
    // Specify number of elements expected in class (ideally some constexpr)
    template<> struct tuple_size<C>
    {  
        typedef int type; 
        static const type value = 2;
    };
}

Finally, we specialize std::tuple_element, which indicates the type of nth element in this class

namespace std
{
    // Specify the type of element n
    template<size_t N> struct tuple_element<N, C>
    {
        typedef decltype(get<N>(declval<C>())) type;
    };
}

We can now use structured binding on objects of this class

C c(1, '2');
auto [a, b] = c; // a = 1, b = '2'

Footnotes

  1. From section 11.5 of C++ Standard draft: if the qualified-id std::tuple_size names a complete type, the expression std::tuple_-size::value shall be a well-formed integral constant expression and the number of elements in the identifier-list shall be equal to the value of that expression. The unqualified-id get is looked up in the scope of E by class member access lookup (6.4.5), and if that finds at least one declaration, the initializer is e.get(). Otherwise, the initializer is get(e), where get is looked up in the associated namespaces (6.4.2). In either case, get is interpreted as a template-id. In either case, e is an lvalue if the type of the entity e is an lvalue reference and an xvalue otherwise. Given the type Ti designated by std::tuple_element<i, E>::type, each vi is a variable of type “reference to Ti ” initialized with the initializer, where the reference is an lvalue reference if the initializer is an lvalue and an rvalue reference otherwise; the referenced type is Ti .