Jonathan Boccara's blog

How to Return Several Values from a Function in C++

Published July 9, 2021 - 0 Comments

Functions should take their inputs as parameters and produce outputs with their return types. This is the basics of functions interface design.

This makes functions easier to understand just by looking at their prototype. It makes functions functional.

But C++ only allows to return one value out of a function. What if we’d like to return several values from a function? And what if we’d also like to avoid extra copies and make sure the RVO applies? And what if, on top of all that, we’d like the code to be expressive?

This is the question Fluent C++ reader Vamsi wrote to me. Let’s see how to return several parameters from a function and respect all the above.

The bad reflex to return several parameters

One way to make a function produce several parameters and work around the fact that the return type contains only one value is to use something else than the return type to produce outputs.

This is bad practice, because as we mentioned outputs should come out of the return type.

This other tempting position than the return type to produce several outputs is to put them in the parameters of the function. To achieve this we can declare the parameters as non-const references:

void f(Foo& foo, Bar& bar)
{
    // fill foo and bar...

This is bad code because the parameters are reserved for inputs (and potentially input-outputs, the existing values that the function modifies), and not for outputs.

What to do then?

Returning a bundle

A simple approach to use the return type is to return one value that contains several values. This can be a std::pair or std::tuple. To keep examples simple we’ll use pair but everything that follows is also valid for std::tuples for more than two returned values.

Let’s consider a function that returns a pair of values:

std::pair<Foo, Bar> f()
{
    Foo foo{};
    Bar bar{};

    // fill foo and bar...

    return {foo, bar};
}

The call site can retrieve those values with structured bindings:

auto [foo, bar] = f();

Structured bindings appeared in C++17. If you’re not in C++17 yet, you can use C++11’s std::tie:

Foo foo{};
Bar bar{};
std::tie(foo, bar) = f();

Avoiding copies

In C++11 or in C++17, this code can incur more copies (or moves) than you’d like. Perhaps you won’t notice a difference because, in all likelihood according to the 80-20 rule, this function won’t be in a performance critical section of the codebase.

But in case it happens to be in a critical section, and some of the involved types are not moveable (for example, if Foo is a legacy type implementing copy constructors and not move constructors, or if Foo is std::array), it is good to know how to avoid unnecessary copies.

Let’s have another look at the code of f:

std::pair<Foo, Bar> f()
{
    Foo foo{};
    Bar bar{};

    // fill foo and bar...

    return {foo, bar};
}

After constructing a Foo and working on it, we copy it into the pair. There is therefore one copy for each element of the pair.

The last line returns a temporary object (of type std::pair<Foo, Bar>). The compiler can apply NRVO and elide copies from this pair created inside the function to the temporary pair returned from the function.

At call site, the structured binding retrieves this pair an initialises individual references from it. Indeed, the following code

auto [foo, bar] = f();

is equivalent to this one:

std::pair<Foo, Bar> result = f();
auto& foo = p.first;
auto& bar = p.second;

The first line does not incur a copy thanks to NRVO. The other lines do not make copies either because they’re only creating references.

In total, there is therefore one copy, when creating the pair inside of f.

How can we avoid this copy? We can create the pair at the beginning of f and work on its elements directly:

std::pair<Foo, Bar> f()
{
    std::pair<Foo, Bar> result;

    // fill result.first and result.second...

    return result;
}

But then the code becomes less expressive because instead of working on foo and bar, the code operates on result.first and result.second which don’t have a lot of meaning.

How can we remedy to that? There are at least two options.

The first one is to take inspiration from the structured bindings. We can introduce references inside of the functions that point to the values inside of the pair. Those references allow to introduce names, to make the code more expressive:

std::pair<Foo, Bar> f()
{
    std::pair<Foo, Bar> result;
    auto& foo = result.first;
    auto& bar = result.second;

    // fill foo and bar...

    return result;
}

Another option is to use a struct, as we’ll see in a moment.

Returning several values of the same type

Using explicit names (rather than result.first and result.second) also reduces the risk of mixing up by mistake the values inside of the function, especially if Foo and Bar are in fact the same type.

Using references with good names inside the function allows to clarify which objects the code is operating on, and makes errors more obvious than when using .first and .second.

But at call site, returning a pair or tuple with several objects of the same type creates a risk of mixing up the results:

auto [foo, bar] = f(); // or should it be [bar, foo]?

In this case, it is best to clarify the identity of each returned value with a name. One way to do this is to use a struct:

struct Results
{
    Foo foo;
    Bar bar;
};

To maintain the return value optimisations we use this struct both inside the function’s implementation and in the function’s prototype:

Results f()
{
    Results results;

    // fill results.foo and results.bar...

    return results;
}

Inside the function, using a struct replaces the local references we mentioned earlier.

Another idea could be to use strong types, as they’re known to make interfaces clearer and safer. Strong types did help when we used std::tie, because we had to define the types of the objects explicitly before calling it. But with structured bindings, they help less because we can still mix up the types we retrieve from the function. If you’d like to dig more, strong types and return values is a whole topic in itself.

Make it easy to retrieve the outputs of your function

C++ offers various ways to return several values form a function, albeit not in a native manner.

Take advantage of them by choosing the most expressive one for your given case. In the vast majority of cases you can get away with a clear interface at no performance cost, and without resorting to passing outputs as parameters.

You will also like

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