Reader small image

You're reading from  Modern C++ Programming Cookbook - Third Edition

Product typeBook
Published inFeb 2024
PublisherPackt
ISBN-139781835080542
Edition3rd Edition
Right arrow
Author (1)
Marius Bancila
Marius Bancila
author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila

Right arrow

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
Previous PageNext Page
You have been reading a chapter from
Modern C++ Programming Cookbook - Third Edition
Published in: Feb 2024Publisher: PacktISBN-13: 9781835080542
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Marius Bancila

Marius Bancila is a software engineer with two decades of experience in developing solutions for line of business applications and more. He is the author of The Modern C++ Challenge and Template Metaprogramming with C++. He works as a software architect and is focused on Microsoft technologies, mainly developing desktop applications with C++ and C#. He is passionate about sharing his technical expertise with others and, for that reason, he has been recognized as a Microsoft MVP for C++ and later developer technologies since 2006. Marius lives in Romania and is active in various online communities.
Read more about Marius Bancila