- Modern C++ Programming Cookbook
- Marius Bancila
- 1057字
- 2021-06-11 18:22:14
Using structured bindings to handle multi-return values
Returning multiple values from a function is very common, yet there is no first-class solution in C++ to make it possible in a straightforward way. Developers have to choose between returning multiple values through reference parameters to a function, defining a structure to contain the multiple values, or returning an std::pair or std::tuple. The first two use named variables, which gives them the advantage that they clearly indicate the meaning of the return value, but have the disadvantage that they have to be explicitly defined. std::pair has its members called first and second, while std::tuple has unnamed members that can only be retrieved with a function call, but can be copied to named variables using std::tie(). None of these solutions are ideal.
C++17 extends the semantic use of std::tie() into a first-class core language feature that enables unpacking the values of a tuple into named variables. This feature is called structured bindings.
Getting ready
For this recipe, you should be familiar with the standard utility types std::pair and std::tuple and the utility function std::tie().
How to do it...
To return multiple values from a function using a compiler that supports C++17, you should do the following:
- Use an std::tuple for the return type:
std::tuple<int, std::string, double> find() { return std::make_tuple(1, "marius", 1234.5); }
- Use structured bindings to unpack the values of the tuple into named objects:
auto [id, name, score] = find();
- Use decomposition declaration to bind the returned values to the variables inside an if statement or switch statement:
if (auto [id, name, score] = find(); score > 1000) { std::cout << name << '\n'; }
How it works...
Structured bindings are a language feature that works just like std::tie(), except that we don't have to define named variables for each value that needs to be unpacked explicitly with std::tie(). With structured bindings, we define all the named variables in a single definition using the auto specifier so that the compiler can infer the correct type for each variable.
To exemplify this, let's consider the case of inserting items into an std::map. The insert method returns an std::pair containing an iterator for the inserted element or the element that prevented the insertion, and a Boolean indicating whether the insertion was successful or not. The following code is very explicit and the use of second or first->second makes the code harder to read because you need to constantly figure out what they represent:
std::map<int, std::string> m;
auto result = m.insert({ 1, "one" });
std::cout << "inserted = " << result.second << '\n'
<< "value = " << result.first->second << '\n';
The preceding code can be made more readable with the use of std::tie, which unpacks tuples into individual objects (and works with std::pair because std::tuple has a converting assignment from std::pair):
std::map<int, std::string> m;
std::map<int, std::string>::iterator it;
bool inserted;
std::tie(it, inserted) = m.insert({ 1, "one" });
std::cout << "inserted = " << inserted << '\n'
<< "value = " << it->second << '\n';
std::tie(it, inserted) = m.insert({ 1, "two" });
std::cout << "inserted = " << inserted << '\n'
<< "value = " << it->second << '\n';
The code is not necessarily simpler because it requires defining the objects that the pair is unpacked to in advance. Similarly, the more elements the tuple has, the more objects you need to define, but using named objects makes the code easier to read.
C++17 structured bindings elevates unpacking tuple elements into named objects to the rank of a language feature; it does not require the use of std::tie(), and objects are initialized when declared:
std::map<int, std::string> m;
{
auto [it, inserted] = m.insert({ 1, "one" });
std::cout << "inserted = " << inserted << '\n'
<< "value = " << it->second << '\n';
}
{
auto [it, inserted] = m.insert({ 1, "two" });
std::cout << "inserted = " << inserted << '\n'
<< "value = " << it->second << '\n';
}
The use of multiple blocks in the preceding example is necessary because variables cannot be redeclared in the same block, and structured bindings imply a declaration using the auto specifier. Therefore, if you need to make multiple calls, as in the preceding example, and use structured bindings, you must either use different variable names or multiple blocks. An alternative to that is to avoid structured bindings and use std::tie(), because it can be called multiple times with the same variables, so you only need to declare them once.
In C++17, it is also possible to declare variables in if and switch statements in the form if(init; condition) and switch(init; condition), respectively. This could be combined with structured bindings to produce simpler code. Let's look at an example:
if(auto [it, inserted] = m.insert({ 1, "two" }); inserted)
{ std::cout << it->second << '\n'; }
In the preceding snippet, we attempted to insert a new value into a map. The result of the call is unpacked into two variables, it and inserted, defined in the scope of the if statement in the initialization part. Then, the condition of the if statement is evaluated from the value of the inserted variable.
There's more...
Although we focused on binding names to the elements of tuples, structured bindings can be used in a broader scope because they also support binding to array elements or data members of a class. If you want to bind to the elements of an array, you must provide a name for every element of the array; otherwise, the declaration is ill-formed. The following is an example of binding to array elements:
int arr[] = { 1,2 };
auto [a, b] = arr;
auto& [x, y] = arr;
arr[0] += 10;
arr[1] += 10;
std::cout << arr[0] << ' ' << arr[1] << '\n'; // 11 12
std::cout << a << ' ' << b << '\n'; // 1 2
std::cout << x << ' ' << y << '\n'; // 11 12
In this example, arr is an array with two elements. We first bind a and b to its elements, and then we bind the x and y references to its elements. Changes that are made to the elements of the array are not visible through the variables a and b but are visible through the x and y references, as shown in the comments that print these values to the console. This happens because when we do the first binding, a copy of the array is created and a and b are bound to the elements of the copy.
As we already mentioned, it's also possible to bind to data members of a class. The following restrictions apply:
- Binding is possible only for non-static members of the class.
- The class cannot have anonymous union members.
- The number of identifiers must match the number of non-static members of the class.
The binding of identifiers occurs in the order of the declaration of the data members, which can include bitfields. An example is shown here:
struct foo
{
int id;
std::string name;
};
foo f{ 42, "john" };
auto [i, n] = f;
auto& [ri, rn] = f;
f.id = 43;
std::cout << f.id << ' ' << f.name << '\n'; // 43 john
std::cout << i << ' ' << n << '\n'; // 42 john
std::cout << ri << ' ' << rn << '\n'; // 43 john
Again, changes to the foo object are not visible to the variables i and n but are to ri and rn. This is because each identifier in the structure binding becomes the name of an lvalue that refers to a data member of the class (just like with an array, it refers to an element of the array). However, the reference type of an identifier is the corresponding data member (or array element).
The new C++20 standard has introduced a series of improvements to structure bindings, including the following:
- Possibility to include the static or thread_local storage-class specifiers in the declaration of the structure bindings.
- Allow the use of the [[maybe_unused]] attribute for the declaration of a structured binding. Some compilers, such as Clang and GCC, already supported this feature.
- Allow us to capture structure binding identifiers in lambdas. All identifiers, including those bound to bitfields, can be captured by value. On the other hand, all identifiers except for those bound to bitfields can also be captured by reference.
These changes enable us to write the following:
foo f{ 42, "john" };
auto [i, n] = f;
auto l1 = [i] {std::cout << i; };
auto l2 = [=] {std::cout << i; };
auto l3 = [&i] {std::cout << i; };
auto l4 = [&] {std::cout << i; };
These examples show the various ways structured bindings can be captured in lambdas in C++20.
See also
- Using auto whenever possible to understand how automatic type deduction works in C++
- Using lambdas with standard algorithms in Chapter 3, Exploring Functions to learn how lambdas can be used with standard library general-purpose algorithms
- Providing metadata to the compiler with attributes in Chapter 4, Preprocessing and Compilation, to learn about providing hints to the compiler with the use of standard attributes
- Oracle WebLogic Server 12c:First Look
- 大學計算機基礎(第三版)
- MongoDB for Java Developers
- 名師講壇:Java微服務架構實戰(SpringBoot+SpringCloud+Docker+RabbitMQ)
- Learning Python by Building Games
- Java EE 7 Performance Tuning and Optimization
- Spring Boot企業級項目開發實戰
- Clojure Reactive Programming
- Emgu CV Essentials
- 編程可以很簡單
- C++從入門到精通(第6版)
- Training Systems Using Python Statistical Modeling
- Java7程序設計入門經典
- C#面向對象程序設計(第2版)
- Selenium Essentials