Jonathan Boccara's blog

Using Strong Types to Return Multiple Values

Published November 10, 2017 - 14 Comments

We’ve seen how strong types helped clarifying function interfaces by being explicit about what input parameters the function expected. Now let’s examine how strong types help clarifying functions that return several outputs.

We’ll start by describing the various ways to return several outputs from a function in C++, and then see how strong types offer an interesting alternative.

Multiple return values in C++

Even though, strictly speaking, C++ doesn’t let functions return several values, some techniques to circumvent this have appeared over time. And some even made their way into becoming native features of the language.

Let’s take the example of function f that takes an Input, and we would like it to return two outputs: an output1 and an output2, which are both of type Output.

Returning a struct

This is the oldest way, but that still works the best in some cases. It consists in creating a struct, which represents a bundle of data, that contains an Output1 and an Output2:

struct Outputs
{
    Output output1;
    Output output2;

    Outputs(Output const& output1, Output const& output2) : output1(output1), output2(output2){}
};

In C++03, adding a constructor makes it syntactically easier to set its values:

Outputs f(Input const& input)
{
    // working out the values
    // of output1 and output2...

    return Outputs(output1, output2);
}

Note that in C++11 we can omit the struct‘s constructor and use extended initializer lists to fill the struct:

Outputs f(Input const& input)
{
    // working out the values
    // of output1 and output2...

    return {output1, output2};
}

Anyway, to retrieve the outputs at call site we simply get the members out of the struct:

auto outputs = f(input);

auto output1 = outputs.output1;
auto output2 = outputs.output2;

Advantages of the struct:

  • the results coming out of the function appear with their names at call site,
  • exists in all versions of C++.

Drawbacks of the struct:

  • needs to define it (and, in C++03, its constructor) for the purpose of the function.

std::tieing to a tuple

Another way to output several values is to return a std::tuple, which can be perceived as an on-the-fly struct. So we throw away our Outputs struct, and our function becomes:

std::tuple<Output, Output> f(Input const& input)
{
    // working out the values
    // of output1 and output2...
    
    return {output1, output2};
}

At call site there are several ways to retrieve the results. One way is to use the accessors of std::tuple: the std::get template functions:

auto output = f(input);

auto output1 = std::get<0>(output);
auto output2 = std::get<1>(output);

But there is a problem here: we have lost track of the order of the values returned by the function.

We’re assuming that output1 comes first and output2 second, but if we get that order wrong (especially in production code where they are hopefully not called output 1 and 2) or if it comes to change, even by mistake, the compiler won’t stop us.

So we’re receiving data from a function but can’t really see that data. It’s a bit like catching a ball with your eyes closed: you need to be very, very confident towards the person who throws it at you.

Multiple return types C++

This problem is mitigated if the outputs are of different types. Indeed, mixing them up would probably lead to a compilation error further down the codeline. But if they are of the same type, like in this example, there is a real risk of mixing them up.

There is another syntax for this technique, using std::tie, that is more pleasant to the eye but has the same risk of mixing up the values:

Output output1;
Output output2;

std::tie(output1, output2) = f(input);

std::tie creates a tuple of references bound to output1 and output2. So copying the tuple coming out of f into this tuple of references actually copies the value inside the tuple into output1 and output2.

std::tie also has the drawback of needing the outputs to be instantiated before calling the function. This can be more or less practical depending on the type of the outputs, and adds visual noise (er- actually, is there such a thing as visual noise? noise is something you’re supposed to hear isn’t it?).

Advantages of std::tie:

  • no need for a struct.

Drawbacks of std::tie:

  • the meaning of each returned value is hidden at call site,
  • needs to instantiate output values before calling the function,
  • visual noise,
  • needs C++11 (not everyone has it yet in production).

Structured bindings

Structured bindings are part of the spearhead of C++17 features. They have a lot in common with std::tie, except they’re easier to use in that they don’t need the outputs to be previously instantiated:

auto [output1, output2] = f(input);

Which makes for a beautiful syntax. But if the outputs are of the same type, we still have the issue of not knowing if the order of the return values is the right one!

Advantages of structured bindings:

  • no need for a struct
  • no need to instantiate output values before calling the function,
  • beautiful syntax

Drawbacks of structured bindings:

  • the meaning of each returned value is hidden at call site,
  • needs C++17 (really not everyone has it yet in production)

Multiple Strong return types

This need of disambiguating several return values of the same type sounds very similar to the one of clarifying the meaning of a function’s parameters, which we solved with strong types.

So let’s use strong types to add specific meaning to each of the return value of our function, by using the NamedType library:

using Output1 = NamedType<Output, struct Output1Tag>;
using Output2 = NamedType<Output, struct Output2Tag>;

Our function can then return those strong types instead of just Outputs:

std::tuple<Output1, Output2> f(Input const& input)
{
    // working out the values
    // of output1 and output2...
    
    return {Output1(output1), Output2(output2)};
}

Note that the function’s prototype now shows exactly what outputs the function returns.

At call site, we get an explicit syntax thanks to an overload of std::get that takes a template type, and not a number, that works when every type inside the tuple is unique. Which is our case here, because our purpose is to differentiate every value that the function is returning, by using the type system:

auto outputs = f(input);

auto output1 = std::get<Output1>(outputs);
auto output2 = std::get<Output2>(outputs);

Advantages of strong types:

  • the results coming out of the function appear with their names at call site,
  • the function’s prototype shows the meaning of each of the returned values,
  • no need for a struct,
  • no need to initialize the outputs before calling the function.

Drawbacks of strong types:

  • needs to define strong types for the returned types,
  • not everything in one line at call site,
  • not standard.

Closing up on struct versus strong types

The solution using strong types has some things in common with the solution that uses structs. Indeed, both create dedicated types and allow a call site to identify each of the values returned from a function.

What’s the difference between them? I believe it lies in the function’s prototype:

With structs:

Outputs f(Input const& input);

With strong types:

std::tuple<Output1, Output2> f(Input const& input);

The strong types show every returned value, while the struct has one name to designate them collectively.

Which one is better? It depends.

If there is a name that represents the concept of all that assembled data, then it makes sense to use that name with a struct, and even consider if this isn’t the opportunity to hide them in a class.

On the other hand, if the returned values are not related to each other (other than by the fact they come out of our function) it’s probably better to use strong types and avoid an awkward name to group unrelated concepts.

Also, the strong types could be arguably more reusable than the struct, as another neighbouring function that returns just a subset of them could also use their definition.

Your feedback on all this is welcome. If you want to use strong types, you’ll find the NamedType library in its GitHub repository.

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed