1

The following is an anti-pattern:

auto f() {
  std::vector<int> v(100000);
  return std::move(v); // no need to use std::move thanks to RVO (return value optimization)
}

Using a std::move can even produce worst code (see here)

However, what should I do in the following situation:

auto f() {
  std::vector<int> v0(100000);
  std::vector<int> v1(100000);
  return std::make_pair(std::move(v0),std::move(v1)); // is the move needed?
}
Bérenger
  • 2,678
  • 2
  • 21
  • 42
  • 2
    In the second situation, the move is not **needed**, *per se*. Providing the `move` only makes the code have 200,000 less copies, so it'll be more performant. – Eljay Feb 23 '22 at 17:54

2 Answers2

1

For the second snippet,

auto f() {
  std::vector<int> v0(100000);
  std::vector<int> v1(100000);
  return std::make_pair(std::move(v0),std::move(v1)); // is the move needed?
}

return returns the result of the std::make_pair() function. That's an RValue.

However, the OP's question probably condenses to whether (or why not) Named Return Value Optimization still applies to v0/v1 when returned as a std::pair.

Thereby, it's overlooked that v0/v1 aren't subject of return anymore, but become arguments of std::make_pair(). As such, v0/v1 are LValues – std::move(v0), std::move(v1) have to be applied to turn them into RValues if move-semantic is intended.


Demo on coliru:

#include <iostream>

template <typename T>
struct Vector {
  Vector(size_t n)
  {
    std::cout << "Vector::Vector(" << n << ")\n";
  }
  Vector(const Vector&)
  {
    std::cout << "Vector::Vector(const Vector&)\n";
  }
  Vector(const Vector&&)
  {
    std::cout << "Vector::Vector(const Vector&&)\n";
  }
  
};

auto f1() {
  Vector<int> v(100000);
  return std::move(v); // over-pessimistic
}

auto f2() {
  Vector<int> v(100000);
  return v; // allows NRVO
}

auto f3() {
  Vector<int> v0(100000);
  Vector<int> v1(100000);
  return std::make_pair(v0, v1); // copy constructor called for v0, v1
}    

auto f4() {
  Vector<int> v0(100000);
  Vector<int> v1(100000);
  return std::make_pair(std::move(v0),std::move(v1)); // move constructor called for v0, v1
}

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

int main()
{
  DEBUG(f1());
  DEBUG(f2());
  DEBUG(f3());
  DEBUG(f4());
}

Output:

f1();
Vector::Vector(100000)
Vector::Vector(const Vector&&)
f2();
Vector::Vector(100000)
f3();
Vector::Vector(100000)
Vector::Vector(100000)
Vector::Vector(const Vector&)
Vector::Vector(const Vector&)
f4();
Vector::Vector(100000)
Vector::Vector(100000)
Vector::Vector(const Vector&&)
Vector::Vector(const Vector&&)
Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56
  • Thanks for the explanation. I think you mean regular RVO for `f2` since the optimization is guaranteed in C++11 (where NRVO was not mandated) – Bérenger Feb 23 '22 at 20:52
  • @Bérenger NVRO is not mandated. Nevertheless, I noticed that g++ uses the opportunity even with `-std=c++14` (for a longer time). (With `-std=c++11`, `auto f()` is rejected.) – Scheff's Cat Feb 24 '22 at 06:22
  • @Bérenger You are correct that NVRO is not mandatory. Hence, I wrote `// allows NVRO` in `f2()`. To force RVO, you had to go with [eeroikas answer](https://stackoverflow.com/a/71242145/7478597), of course, i.e. `return std::vector(100000);`. – Scheff's Cat Feb 24 '22 at 06:34
1

Yes, the move is needed to avoid copy in the latter case.

However, this would be even better:

return std::make_pair(
    std::vector<int>(100000),
    std::vector<int>(100000));
eerorika
  • 232,697
  • 12
  • 197
  • 326