Ignorant
Ignorant

Reputation: 2671

When the move constructor is actually called if we have (N)RVO?

I have understood from several questions here on SO that (N)RVO prevents the move constructor from being called, when an object is returned by value. Classic example:

struct Foo {
  Foo()            { std::cout << "Constructed\n"; }
  Foo(const Foo &) { std::cout << "Copy-constructed\n"; }
  Foo(Foo &&)      { std::cout << "Move-constructed\n"; }
  ~Foo()           { std::cout << "Destructed\n"; }
};

Foo makeFoo() {
  return Foo();
}

int main() { 
  Foo foo = makeFoo(); // Move-constructor would be called here without (N)RVO
}

The output with (N)RVO enabled is:

Constructed
Destructed

So in what cases the move constructor would be called, regardless the (N)RVO presence? Could you provide some examples? In other words: why should I care implementing a move constructor if the (N)RVO does its optimization job by default?

Upvotes: 2

Views: 674

Answers (1)

Chris Drew
Chris Drew

Reputation: 15334

First, you should probably make sure that Foo follows the rule of three/five and has move/copy assignment operators. And it is good practice for the move-constructor and move-assignment operator to be noexcept:

struct Foo {
  Foo()                           { std::cout << "Constructed\n"; }
  Foo(const Foo &)                { std::cout << "Copy-constructed\n"; }
  Foo& operator=(const Foo&)      { std::cout << "Copy-assigned\n"; return *this; }
  Foo(Foo &&)            noexcept { std::cout << "Move-constructed\n"; }
  Foo& operator=(Foo &&) noexcept { std::cout << "Move-assigned\n"; return *this; }

  ~Foo()                    { std::cout << "Destructed\n"; }
};

In most cases you can follow the rule of zero and don't actually need to define any of these special member functions, the compiler will create them for you, but it is useful for this purpose.

(N)RVO is only for function return values. It does not apply, for example, for function parameters. Of course the compiler can apply whatever optimizations it likes under the "as-if" rule so we have to be careful when crafting trivial examples.

Function parameters

There are many cases where the move-constructor or move-assignment operator will be called. But a simple case is if you use std::move to transfer ownership to a function that accepts a parameter by-value or by rvalue-reference:

void takeFoo(Foo foo) {
  // use foo...
}

int main() { 
  Foo foo = makeFoo();

  // set data on foo...

  takeFoo(std::move(foo));
}

Output:

Constructed
Move-constructed
Destructed
Destructed

For use in standard library containers

A very useful case for the move-constructor is if you have a std::vector<Foo>. As you push_back objects into the container it occasionally has to re-allocate and move all the existing objects to new memory. If there is a valid move-constructor available on Foo it will use it instead of copying:

int main() { 
  std::vector<Foo> v;
  std::cout << "-- push_back 1 --\n";
  v.push_back(makeFoo());
  std::cout << "-- push_back 2 --\n";
  v.push_back(makeFoo());
}

Output:

-- push_back 1 --
Constructed
Move-constructed  <-- move new foo into container
Destructed        
-- push_back 2 --
Constructed
Move-constructed  <-- move existing foo to new memory
Move-constructed  <-- move new foo into container
Destructed
Destructed
Destructed
Destructed

Constructor member initializer lists

I find move-constructors useful in constructor member initializer lists. Say you have a class FooHolder that contains a Foo. Then you can define a constructor that takes a Foo by-value and moves it into the member variable:

class FooHolder {
  Foo foo_;
public:
  FooHolder(Foo foo) : foo_(std::move(foo)) {} 
};

int main() { 
  FooHolder fooHolder(makeFoo());
}

Output:

Constructed
Move-constructed
Destructed
Destructed

This is nice because it allows me to define a constructor that accepts lvalues or rvalues without unnecessary copies.

Cases that defeat NVRO

RVO always applies but there are cases that defeat NVRO. For example if you have two named variables and the choice of return variable is not known at compile time:

Foo makeFoo(double value) {
  Foo f1;
  Foo f2;
  if (value > 0.5)
    return f1;
  return f2;
}

Foo foo = makeFoo(value);

Output:

Constructed
Constructed
Move-constructed
Destructed
Destructed
Destructed

Or if the return variable is also a function parameter:

Foo appendToFoo(Foo foo) {

  // append to foo...

  return foo;
}

int main() { 
  Foo f1;
  Foo f2 = appendToFoo(f1);
}

Output:

Constructed
Copy-constructed
Move-constructed
Destructed
Destructed
Destructed

Optimizing setters for rvalues

One case for a move-assignment operator is if you want to optimize a setter for rvalues. Say you have a FooHolder that contains a Foo and you want a setFoo member function. Then if you want to optimize for both lvalues and rvalues you should have two overloads. One that takes a reference-to-const and another that takes an rvalue-reference:

class FooHolder {
  Foo foo_;
public:
  void setFoo(const Foo& foo) { foo_ = foo; }
  void setFoo(Foo&& foo) { foo_ = std::move(foo); }
};

int main() { 
  FooHolder fooHolder;  
  Foo f;
  fooHolder.setFoo(f);  // lvalue
  fooHolder.setFoo(makeFoo()); // rvalue
}

Output:

Constructed
Constructed
Copy-assigned  <-- setFoo with lvalue
Constructed
Move-assigned  <-- setFoo with rvalue
Destructed
Destructed
Destructed

Upvotes: 4

Related Questions