Search code examples
c++move-semanticsreturn-value-optimizationnrvo

Returning a pair of objects


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?
}

Solution

  • 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&&)