The latest C++ standard is now upon us, so it’s time to have a look at some of its new features.
To put one of the new features into context we’re going to have a look at – as the title suggests – multiple function return values
I should really distinguish between the following:
- A Subroutine (or Subprogram) is a parameterised block of code that can be called multiple times from within a program.
- A Procedure is a subroutine that may have multiple input and/or output parameters and usually does not return a value. Procedures may change the state of the system
- A Function is a subroutine that has only input parameters and produces a return value. Functions are stateless – they will always produce the same result for the same inputs.
C++ programmers typically blur these distinctions (or ignore them). To keep with the C++ vernacular I will use the term ‘function’ to mean any of the above.
(*Sorry – this is a really terrible pun for a title)
Contents
The multiple-out-parameter dilemma
A C++ function has only one output parameter – the function return value. If we need more than one output from a function (for example, a value and an error code) many programmers will use an input-output parameter for one of the outputs
enum class Error { ok, failed }; double return_many(const double in_val, Error& err); // // err is an input-output parameter // but being used as an output.
Of course, there is nothing to stop us returning a single output value with multiple elements – a structure:
enum class Error { ok, failed }; struct Return_values { double value; Error error; }; Return_values return_many(const double in_val);
This function now has a much clearer intent. There is clean separation between function inputs and outputs. Many programmers resist this style of function, however, feeling it to be inefficient. Used correctly, though, this isn’t the case
int main() { Return_values result = return_many(14.6); if (result.error == Error::ok) { std::cout << result.value << std::endl; } }
We could even exploit Non-Static Data Member Initialisers (NSDMIs) for return value defaults
struct Return_values { double value { }; Error error { Error::ok }; }; Return_values return_many(const double in_val);
The (almost insignificant) wrinkle in all this is that the return value and error code are bound together in the same object. There may be occasions where we want their lifetimes to be independent, in which case we have to extract the individual parts.
int main() { Return_values result = return_many(14.6); Error error { result.error }; double val { result.value }; if (error == Error::ok) { std::cout << val << std::endl; } }
Tuple and tie
C++11 introduced std::tuple as a generic n-tuple type. We could choose to use a tuple as our return value type.
enum class Error { ok, failed }; std::tuple<double, Error> return_many(const double in_val);
Our client code has to be modified now. Since tuples don’t have named elements (unless structs) we have to use a templated utility function – get<> – to extract each element.
int main() { std::tuple<double, Error> result = return_many(100.7); double value { std::get<0>(result) }; Error error { std::get<1>(result) }; if (error == Error::ok) { std::cout << value << std::endl; } }
I’m not sure that’s made our code any more readable. Maybe automatic type-deduction can help a little?
int main() { auto result = return_many(100.7); auto value = std::get<0>(result); auto error = std::get<1>(result); if (error == Error::ok) { std::cout << value << std::endl; } }
This is certainly less verbose, but I have to keep referring back to the function declaration to find out what the types of the result value object elements are.
From C++14, if your tuple has unique types for each of its elements you can specify the type of the element you want to extract instead.
int main() { auto result = return_many(100.7); auto value = std::get<double>(result); auto error = std::get<Error>(result); if (error == Error::ok) { std::cout << value << std::endl; } }
If you don’t have unique types in your tuple your code won’t compile.
A simpler approach is to use some of the std::tuple helper functions.
std::tie assigns tuple values to local variables. It does this by creating a (temporary) tuple of references to the local objects, then assign from the right-hand tuple to the temporary. Since whatever happens to a reference happens to the original object, values from the right-hand tuple are assigned to the local objects. (for an excellent description of how std::tie works have a look here)
Notice, std::tie requires the same number of parameters – in the same order – as the source tuple. std::ignore can be used as a placeholder if you do not wish to retrieve a particular value from the tuple.
int main() { double value { }; auto result = return_many(100.7); std::tie(value, std::ignore) = result; // Ignore the error std::cout << value << std::endl; }
Or, more tersely.
int main() { double value { }; std::tie(value, std::ignore) = return_many(100.7); std::cout << value << std::endl; }
In case you were wondering (and to save you the typing) std::tie only works with std::tuples. The following code won’t compile.
enum class Error { ok, failed }; struct Return_values { double value { }; Error error { Error::ok }; }; Return_values return_many(const double in_val); int main() { double value { }; std::tie(value, std::ignore) = return_many(100.7); // ERROR! std::cout << value << std::endl; }
Structured bindings
Multiple output values via a struct (or tuple) has become an idiom of Modern C++. To improve the readability of code C++17 introduced the concept of Structured Bindings.
Structured bindings extend the mechanism of automatic type-deduction to allow multiple objects to have their types deduced from a tuple.
So, now we can write
enum class Error { ok, failed }; std::tuple<double, Error> return_many(const double in_val); int main() { auto [value, error ] = return_many(100.7); std::cout << value << std::endl; if (error == Error::ok) { std::cout << value << std::endl; } }
The number of objects must match the number of elements in the initialiser. There’s no way to ‘ignore’ a value if you’re not interested in it.
If an object isn’t used the compiler will emit a warning to that effect. To suppress this diagnostic you can use the [[ maybe_unused ]] attribute.
int main() { [[ maybe_unused ]] auto [value, error ] = return_many(100.7); std::cout << value << std::endl; // error isn't used in this code. The [[ maybe_unused ]] // attribute will suppress the compiler diagnostic // // if (error == Error::ok) { // std::cout << value << std::endl; // } }
The nice thing about structured bindings is they work not only with tuples, but on any struct-like construct. So, the following code works just fine.
enum class Error { ok, failed }; struct Return_values { double value { }; Error error { Error::ok }; }; Return_values return_many(const double in_val); int main() { auto [value, error ] = return_many(100.7); std::cout << value << std::endl; if (error == Error::ok) { std::cout << value << std::endl; } }
Structured bindings will also work with arrays (including std::array)
std::array<double, 4> return_many(const double in_val); int main() { auto [val1, val2, val3, val4 ] = return_many(100.7); std::cout << val1 << std::endl; ... }
As with previously, you have to supply as many objects as there are initiliasers.
(And before you ask: no, this won’t work with unions)
Summary
Structured bindings in C++ provide a convenient syntax for returning (output) parameters from a function. This allows the programmer to make a clearer design intent about their function’s parameter usage. Being more explicit is always a better thing when programming.
If you’d like to build up, or expand, your C++ or design skills have a look at the following training links.
Real-Time Software Design with UML
- Practice makes perfect, part 3 – Idiomatic kata - February 27, 2020
- Practice makes perfect, part 2– foundation kata - February 13, 2020
- Practice makes perfect, part 1 – Code kata - January 30, 2020
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.
He specialises in C++, UML, software modelling, Systems Engineering and process development.