Skip to main content

Move constructors, (N)RVO, and non-copyable objects

C++ is a hard language to learn, and a hard language to master. I was reminded of this other day when I came across an incorrect use of move semantics. I saw a method of class that returned an rvalue reference to a stack object (technically it is actually an xvalue). It looked something like this:

Foo && createFoo()
{
  Foo foo(1,2);

  ...

  return std::move(foo);
}

This is undefined behavior, as a reference to a stack object is being returned (thus a dangling reference is the result). I made the same mistake when I started using C++11's features.

A move does take place, but does so after foo is destroyed. What is even worse, is that because we wrap foo in std::move() in the return statement, the compiler does not warn that you are returning a reference to a stack variable. As far as it is concerned, you are just passing a reference into a function, which is no cause for alarm, and returning a reference already returned by another function, which again is a fine thing to do. And depending on what happens in foo's destructor, this code may work symptom free when you run it, and inevitably cause untold disasters when your customer(s) run it.

What the function should be is:

Foo createFoo()
{
  Foo foo(1,2);

  ...

  return foo;
}

Now, if Foo has non-copyable resources (file descriptors, pointers, large amounts of data, etc.), and thus has a deleted or otherwise inaccessible copy constructor, we have a problem. When foo is returned, a temporary Foo is constructed, then assuming we assign it to stack variable in another scope, a third Foo is constructed from the temporary Foo. In C++03 with optimizations turned off (such as NRVO and RVO), this would mean the copy constructor would be invoked twice. Even if you turned on the optimizations, the compiler should complain that the copy constructors are inaccessible. This is really disappointing, because we always want RVO in these situations.

C++11 changed this by adding the move constructors. This means we can make the copy constructor inaccessible, provide a move constructor for Foo, and the above code will compile just fine (and RVO will still be applied if we have the copy elision optimization enabled). And what's even better, is that move constructors are implicit (like copy constructors). Or this would be even better, if it was not for the fact that one of the conditions for implicit move constructors is:

There are no user-declared copy constructors.

So in the case the move constructor is really important to us because we do not want to perform a copy, is one of the cases defined as where we will not get an implicit move constructor. In C++11 there are four such conditions in which a move constructor will not implicitly be declared, and four other conditions under which it will be defined as deleted.

So to allow and enforce copy-free return-by-value in your code, you would want to write Foo as:

class Foo
{
  public:
    ...

    // move constructor
    Foo(
        Foo && old); 


    // delete copy constructor and assignment operator
    Foo(
        Foo const & old) = delete;
    Foo & operator=(
        Foo const & lhs) = delete;

    ...

};

If you have copy elision disabled, your code will still compile, and execute as desired. And if you leave it enabled (as you should), RVO and NRVO will take place. This should allow to quite reliably adhere to RAII without having to return a std::unique_ptr.

Finally, even if you prefer to return std::unique_ptr rather than implement move constructors, the lesson to take away from this is you really should never have a function returning an rvalue. Because either, it is returning a reference to a stack allocation, or it is returning a reference to a member variable which is about to be destroyed and is likely to lead to some difficult bugs to track down when the function gets called twice or in an unintended order.