Returning multiple values from a function is something very common, yet there is no first-class solution in C++ to enable it directly. 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 that have 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
, and 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 is 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 to named variables. This feature is called structured bindings.
For this recipe, you should be familiar with the standard utility types std::pair
and std::tuple
and the utility function std::tie()
.
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 variables inside an
if
statement orswitch
statement.
if (auto [id, name, score] = find(); score > 1000) { std::cout << name << std::endl; }
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 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 in a std::map
. The insert method returns a std::pair
containing an iterator to 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 << std::endl << "value = " << result.first->second << std::endl;
The preceding code can be made more readable with the use of std::tie
, that 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 << std::endl << "value = " << it->second << std::endl; std::tie(it, inserted) = m.insert({ 1, "two" }); std::cout << "inserted = " << inserted << std::endl << "value = " << it->second << std::endl;
The code is not necessarily simpler because it requires defining in advance the objects that the pair is unpacked to. 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 the unpacking of 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 << std::endl << "value = " << it->second << std::endl; } { auto[it, inserted] = m.insert({ 1, "two" }); std::cout << "inserted = " << inserted << std::endl << "value = " << it->second << std::endl; }
The use of multiple blocks in the above 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 like in the example above and use structured bindings you must either use different variable names or multiple blocks as shown above. An alternative to that is to avoid structured bindings and use std::tie()
, because it can be called multiple times with the same variables, therefore you only need to declare them once.
In C++17, it is also possible to declare variables in if
and switch
statements with the form if(init; condition)
and switch(init; condition)
. This could be combined with structured bindings to produce simpler code. In the following example, we attempt 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. The condition of the if
statement is evaluated from the value of the inserted object:
if(auto [it, inserted] = m.insert({ 1, "two" }); inserted) { std::cout << it->second << std::endl; }