Search

15.3 — Move constructors and move assignment

In lesson 15.1 -- Intro to smart pointers and move semantics, we took a look at std::auto_ptr, discussed the desire for move semantics, and took a look at some of the downsides that occur when functions designed for copy semantics (copy constructors and copy assignment operators) are redefined to implement move semantics.

In this lesson, we’ll take a deeper look at how C++11 resolves these problems via move constructors and move assignment.

Copy constructors and copy assignment

First, let’s take a moment to recap copy semantics.

Copy constructors are used to initialize a class by making a copy of an object of the same class. Copy assignment is used to copy one class to another existing class. By default, C++ will provide a copy constructor and copy assignment operator if one is not explicitly provided. These compiler-provided functions do shallow copies, which may cause problems for classes that allocate dynamic memory. So classes that deal with dynamic memory should override these functions to do deep copies.

Returning back to our Auto_ptr smart pointer class example from the first lesson in this chapter, let’s look at a version that implements a copy constructor and copy assignment operator that do deep copies, and a sample program that exercises them:

In this program, we’re using a function named generateResource() to create a smart pointer encapsulated resource, which is then passed back to function main(). Function main() then assigns that to an existing Auto_ptr3 object.

When this program is run, it prints:

Resource acquired
Resource acquired
Resource destroyed
Resource acquired
Resource destroyed
Resource destroyed

That’s a lot of resource creation and destruction going on for such a simple program! What’s going on here?

Let’s take a closer look. There are 6 key steps that happen in this program (one for each printed message):

1) Inside generateResource(), local variable res is created and initialized with a dynamically allocated Resource, which causes the first “Resource acquired”.
2) Res is returned back to main() by value. We return by value here because res is a local variable -- it can’t be returned by address or reference because res will be destroyed when generateResource() ends. So res is copy constructed into a temporary object. Since our copy constructor does a deep copy, a new Resource is allocated here, which causes the second “Resource acquired”.
3) Res goes out of scope, destroying the originally created Resource, which causes the first “Resource destroyed”.
4) The temporary object is assigned to mainres by copy assignment. Since our copy assignment also does a deep copy, a new Resource is allocated, causing yet another “Resource acquired”.
5) The assignment expression ends, and the temporary object goes out of expression scope and is destroyed, causing a “Resource destroyed”.
6) At the end of main(), mainres goes out of scope, and our final “Resource destroyed” is displayed.

So, in short, because we call the copy constructor once to copy construct res to a temporary, and copy assignment once to copy the temporary into mainres, we end up allocating and destroying 3 separate objects in total.

Inefficient, but at least it doesn’t crash!

However, with move semantics, we can do better.

Move constructors and move assignment

C++11 defines two new functions in service of move semantics: a move constructor, and a move assignment operator. Whereas the goal of the copy constructor and copy assignment is to make a copy of one object to another, the goal of the move constructor and move assignment is to move the resources from one object to another.

Defining a move constructor and move assignment work analogously to their copy counterparts. However, whereas the copy flavors of these functions take a const l-value reference parameter, the move flavors of these functions use non-const r-value reference parameters.

Here’s the same Auto_ptr3 class as above, with a move constructor and move assignment operator added. We’ve left in the deep-copying copy constructor and copy assignment operator for comparison purposes.

The move constructor and move assignment operator are simple. Instead of deep copying the source object (a) into the implicit object, we simply move (steal) the source object’s resources. This involves shallow copying the source pointer into the implicit object, then setting the source pointer to null.

When run, this program prints:

Resource acquired
Resource destroyed

That’s much better!

The flow of the program is exactly the same as before. However, instead of calling the copy constructor and copy assignment operators, this program calls the move constructor and move assignment operators. Looking a little more deeply:

1) Inside generateResource(), local variable res is created and initialized with a dynamically allocated Resource, which causes the first “Resource acquired”.
2) Res is returned back to main() by value. Res is move constructed into a temporary object, transferring the dynamically created object stored in res to the temporary object.
3) Res goes out of scope. Because res no longer manages a pointer (it was moved to the temporary), this is a non-event.
4) The temporary object is move assigned to mainres. This transfers the dynamically created object stored in the temporary to mainres.
5) The assignment expression ends, and the temporary object goes out of expression scope and is destroyed. However, because the temporary no longer manages a pointer (it was moved to mainres), this is also a non-event.
6) At the end of main(), mainres goes out of scope, and our final “Resource destroyed” is displayed.

So instead of copying our Resource twice (once for the copy constructor and once for the copy assignment), we transfer it twice. This is more efficient, as Resource is only constructed and destroyed once instead of three times.

In most cases, a move constructor and move assignment operator will not be provided by default, unless the class does not have any define copy constructors, copy assignment, move assignment, or destructors. However, the default move constructor and move assignment do the same thing as the default copy constructor and copy assignment (make copies, not do moves).

Rule: If you want a move constructor and move assignment that do moves, you’ll need to write them yourself.

When are the move constructor and move assignment called?

The move constructor and move assignment are called when those functions have been defined, and the argument for construction or assignment is an r-value. Most typically, this r-value will be a literal or temporary value.

The key insight behind move semantics

You now have enough context to understand the key insight behind move semantics.

If we construct an object or do an assignment where the argument is an l-value, the only thing we can reasonably do is copy the l-value. We can’t assume it’s safe to alter the l-value, because it may be used again later in the program. If we have an expression “a = b”, we wouldn’t reasonably expect b to be changed in any way.

However, if we construct an object or do an assignment where the argument is an r-value, then we know that r-value is just a temporary object of some kind. Instead of copying it (which can be expensive), we can simply transfer its resources (which is cheap) to the object we’re constructing or assigning. This is safe to do because the temporary will be destroyed at the end of the expression anyway, so we know it will never be used again!

C++11, through r-value references, gives us the ability to provide different behaviors when the argument is an r-value vs an l-value, enabling us to make smarter and more efficient decisions about how our objects should behave.

Automatic l-values returned by value may be moved instead of copied

In the generateResource() function of the Auto_ptr4 example above, when variable res is returned by value, it is moved instead of copied, even though res is an l-value. The C++ specification has a special rule that says automatic objects returned from a function by value can be moved even if they are l-values (if the copy isn’t elided altogether). This makes sense, since res was going to be destroyed at the end of the function anyway! We might as well steal its resources instead of making an expensive and unnecessary copy.

Disabling copying

In the Auto_ptr4 class above, we left in the copy constructor and assignment operator for comparison purposes. But in move-enabled classes, it is sometimes desirable to delete the copy constructor and copy assignment functions to ensure copies aren’t made. In the case of our Auto_ptr class, we don’t want to copy our templated object T -- both because it’s expensive, and whatever class T is may not even support copying!

Here’s a version of Auto_ptr that supports move semantics but not copy semantics:

If you were to try to pass an Auto_ptr5 l-value to a function by value, the compiler would complain that the copy constructor required to initialize the copy constructor argument has been deleted. This is good, because we should probably be passing Auto_ptr5 by const l-value reference anyway!

Auto_ptr5 is (finally) a good smart pointer class. And, in fact the standard library contains a class very much like this one (that you should use instead), named std::unique_ptr. We’ll talk more about std::unique_ptr later in this chapter.

Another example

Let’s take a look at another class that uses dynamic memory: a simple dynamic templated array. This class contains a deep-copying copy constructor and copy assignment operator.

Now let’s use this class in a program. To show you how this class performs when we allocate a million integers on the heap, we’re going to leverage a new class, named Timer. We’ll use the Timer class to time how fast our code runs, and show you the performance difference between copying and moving.

On one of the author’s machines, in release mode, this program executed in 0.00825559 seconds.

Now let’s run the same program again, replacing the copy constructor and copy assignment with a move constructor and move assignment.

On the same machine, this program executed in 0.0056 seconds.

Comparing the runtime of the two programs, 0.0056 / 0.00825559 = 67.8%. The move version was almost 33% faster!

15.4 -- std::move
Index
15.2 -- R-value references

11 comments to 15.3 — Move constructors and move assignment

  • mickun

    Hi Alex,

    In Auto_ptr4, in function generateResource(), you wrote that return statement will invoke the move constructor. The res object is an l-value in this function, so how can that be the case? Shouldn’t the copy constructor be invoked because res is an l-value?

    • Alex

      This is a really good question. I added a new section to the article titled, “Automatic l-values returned by value may be moved instead of copied” to address this point. Let me know if you have any further questions.

  • Jason

    Hi alex, 2 questions:

    1) at the end of ‘Disabling copying’ section you write:

      "This is good, because we should probably be passing Auto_ptr5 by const l-value reference anyway!"
       What do you mean by this?

    2) Consider the code snippet:
      
       Auto_ptr5& operator=(Auto_ptr5&& a)
        {
            // Self-assignment detection
            if (&a == this)
                return *this;
     
            // Release any resource we’re holding
            delete m_ptr;
      
      My question is why do the self-assignment detection when &a /=  this??

      Tnanx Jason.

    • Alex

      1) We don’t want to pass Auto_ptr5 by value, both because it invokes copy semantics, and because it’s inefficient. If we want to pass an Auto_ptr5 to a function without having the function take ownership, we should be passing it in by reference (l-value reference, not r-value reference).

      2) So that if the caller does something like w = std::move(w); m_ptr isn’t deleted and your class is left in a consistent state.

  • rymcymcym

    Could you expllain me why in Auto_ptr4, move assignment has been called? The res object is l_value, so why this temporary object was created with r_value?

    • Alex

      res is an l-value, but the return value of the function is being returned by value. This return value is an r-value, since it is just a temporary.

  • SU

    I am trying to prove the default move assignment will not be provided if I have defined copy constructor or copy assignment or destructor, I modified an example online, which has defined copy constructor and destructor. But in main function when I try to use move assignment to do e1 = Example5("d"), the compiler does not complain anything, and the program ran well.

    So this is obviously contradict to the statement "a move constructor and move assignment operator will not be provided by default, unless the class does not have any define copy constructors, copy assignment, move assignment, or destructors."

    • Alex

      No, it’s not contradicting. Your program, not finding a move assignment operator, is defaulting to copy assignment (which is provided by default).

  • Xiao Hu

    I think the implement of Move assignment in DynamicArray has some problems:
    you don’t free the memory of original m_array, you might use delete[] m_array.
    Also, I think that a nice code can be:

    • Alex

      Whoops, fixed the issue with the missing delete[]. Thanks for pointing that out.

      Using std::swap here only works if the implicit array was originally empty. Otherwise, parameter arr ends up with the original array’s length and a dangling pointer. That’s okay if arr was really an r-value that was going out of scope, but if arr was a l-value that had been moved into an r-value, then we’d expect it to end up empty, not invalid.

      • Xiao Hu

        I still think that using std::swap has advantage:
        uisng std::swap, the T* object of *this and arr is swapped. Regardless of the arr is a real l-value or r-value, the arr is destoryed once it goes out of scope. So, the orignal T* object of *this is destoryed. Using std::swap, we don’t need to care how to destory the T* object.
        Another problem is that the running time of move version is longer than the one of copy version. In fact, the result in my machine is instead opposite.

Leave a Comment

Put C++ code inside [code][/code] tags to use the syntax highlighter