Book Image

Modern C++ Programming Cookbook - Third Edition

By : Marius Bancila
Book Image

Modern C++ Programming Cookbook - Third Edition

By: Marius Bancila

Overview of this book

The updated third edition of Modern C++ Programming Cookbook addresses the latest features of C++23, such as the stack library, the expected and mdspan types, span buffers, formatting library improvements, and updates to the ranges library. It also gets into more C++20 topics not previously covered, such as sync output streams and source_location. The book is organized in the form of practical recipes covering a wide range of real-world problems. It gets into the details of all the core concepts of modern C++ programming, such as functions and classes, iterators and algorithms, streams and the file system, threading and concurrency, smart pointers and move semantics, and many others. You will cover the performance aspects of programming in depth, and learning to write fast and lean code with the help of best practices. You will explore useful patterns and the implementation of many idioms, including pimpl, named parameter, attorney-client, and the factory pattern. A chapter dedicated to unit testing introduces you to three of the most widely used libraries for C++: Boost.Test, Google Test, and Catch2. By the end of this modern C++ programming book, you will be able to effectively leverage the features and techniques of C++11/14/17/20/23 programming to enhance the performance, scalability, and efficiency of your applications.
Table of Contents (15 chapters)
13
Other Books You May Enjoy
14
Index

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 a 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() to 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:

  1. Use an std::tuple for the return type:
    std::tuple<int, std::string, double> find()
    {
      return {1, "marius", 1234.5};
    }
    
  2. Use structured bindings to unpack the values of the tuple into named objects:
    auto [id, name, score] = find();
    
  3. Use structure bindings 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 (sometimes referred to as decomposition declaration) 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 a std::map. The insert method returns a 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 elevate unpacking tuple elements into named objects to the rank of a language feature; there is no requirement for 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 they are 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 <<''\'';           // 42 john
std::cout << ri <<'''' << rn <<''\'';         // 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:

  • The possibility to include the static or thread_local storage-class specifiers in the declaration of the structure bindings.
  • The use of the [[maybe_unused]] attribute for the declaration of a structured binding. Some compilers, such as Clang and GCC, had already supported this feature.
  • The possibility to capture structure binding identifiers in lambdas. All identifiers, including those bound to bitfields, can be captured by value. Conversely, 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.

Sometimes, we need to bind variables that we don’t use. In C++26, it will be possible to ignore a variable by using an underscore (_) instead of a name. Although not supported by any compiler at the time of writing, this feature has been included in C++26.

foo f{ 42,"john" };
auto [_, n] = f;

Here, _ is a placeholder for a variable that is bound to the id member of the foo object. It is used to indicate that this value is not used and will be ignored in this context.

The use of a _ placeholder is not limited to structured bindings. It can be used as an identifier for non-static class members, structured bindings, and lambda captures. You can use an underscore to redefine an existing declaration in the same scope, therefore making it possible to ignore multiple variables. However, a program is considered ill-formed if the variable named _ is used after a redeclaration.

See also

  • Using auto whenever possible, to understand how automatic type deduction works in C++
  • Chapter 3, Using lambdas with standard algorithms, to learn how lambdas can be used with standard library general-purpose algorithms
  • Chapter 4, Providing metadata to the compiler with attributes, to learn about providing hints to the compiler with the use of standard attributes