pnklein
pnklein

Reputation: 985

In c++, how to return multiple objects and nevertheless benefit from RVO

My function needs to return several large containers. To achieve this, the return statement creates a tuple consisting of the containers to return. However, based on my tests (using Apple clang version 11.0.0, clang-1100.0.33.17), copies are made, presumably when the tuple is constructed. In the calling function, the returned tuple is assigned using structured bindings to several variables.

If my function returned only one container, RVO would be used, and no copies would be made. Is there a nice way to similarly avoid creating copies when a function returns several containers?

Example below:

#include <tuple>
#include <vector>
using namespace std;

tuple<vector<int>, vector<double>> f(){
  vector<int> a(1);
  vector<double> b(1);
  return tuple(a,b);
}

int main(){
  auto [x, y] = f();
}

Upvotes: 4

Views: 495

Answers (2)

n314159
n314159

Reputation: 5075

You can construct the vectors already in the tuple.

tuple<vector<int>, vector<double>> f(){
  tuple ret(vector<int>(1), vector<double>(1));
  // Now usable by std::get<N>(ret);
  auto &[a,b] = ret;
  // Now you can use a and b, no copies made.
  return ret;
}

While this is pretty similar to the other answer I wanted to add the part with the structured binding and an explanation why you basically have to do it in this or a very similar way if you want to have RVO without any moves etc.

For RVO the compiler basically adds an invisible outparameter. It allocates the space needed in the end for the returned value already when calling and passes the location to the callee. Then the callee will use the space for the variable that gets returned. Hence for RVO the return value must be created in the function and the following does not do RVO:

std::string foo(int i) {
  std::string ret1{"Hi"};      // Which one should use the space?
  std::string ret2{"Hello"};
  if( i > 0 ) {                // We only know here
    return ret1;
  } else {
    return ret2;
  }
}

This is also the reason why multiple return values have to be part of the same object when they are created. The space allocated fits exactly the return type and you cannot just take two independent objects and construct them in this space, even when you know that they will be later passed in the returned object.

Upvotes: 2

NicholasM
NicholasM

Reputation: 4673

What if you try moving?

tuple<vector<int>, vector<double>> f(){
  vector<int> a(1);
  vector<double> b(1);
  return tuple(std::move(a), std::move(b));
}

Or more simply, construct the tuple from temporaries (which should also lead to move construction):

using vector_tuple = tuple<vector<int>, vector<double>>;

vector_tuple f(){
  return std::make_tuple<vector_tuple>(vector<int>(1), vector<double>(1));
}

Upvotes: 4

Related Questions