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

(Note: You may only get 4 outputs if your compiler elides the return value from function generateResource())

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.

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.

In most cases, a move constructor and move assignment operator will not be provided by default, unless the class does not have any defined 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.

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.

Move functions should always leave your objects in a well-defined state

In the above examples, both the move constructor and move assignment functions set a.m_ptr to nullptr. This may seem extraneous -- after all, if “a” is a temporary r-value, why bother doing “cleanup” if parameter “a” is going to be destroyed anyway?

For reasons that will become more clear next lesson, it’s a good idea to always leave the objects being stolen from in some well-defined (deterministic) state. Ideally, this should be a “null state”, where the object is set back to its uninitiatized or zero state. We’ll explain why in the next lesson, once we’ve supplied some additional context around how move semantics can be used.

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 the Timer class we developed in lesson 8.16 -- Timing your code. 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

34 comments to 15.3 — Move constructors and move assignment

  • AMG

    Hi Alex,
    Back to Chapter 15.1:"First, because std::auto_ptr implements move semantics through the copy constructor and assignment operator, passing a std::auto_ptr by value to a function will cause your resource to get moved to the function parameter (and be destroyed at the end of the function when the function parameters goes out of scope). Then when you go to access your auto_ptr argument from the caller (not realizing it was transferred and deleted), you’re suddenly dereferencing a null pointer. Crash!" Seemed Auto_ptr5 will have the same problem: resources of Auto_ptr5 will be passed to the function parameter. Is it correct? Thanks.

    • Alex

      No. If you try to pass an Auto_ptr5 l-value by value, the compiler will complain that you’re trying to do something that requires using the deleted copy constructor. Auto_ptr5 will only work if the argument is an r-value, and if that’s the case, then you wouldn’t be able to call it later anyway.

  • hi
    i comment some of the first program code and get the same result
    i think  this code did not do any think
    ---------------

    ---------------

    -----------------
    Resource acquired
    Resource acquired
    Resource destroyed
    Resource destroyed

    Process returned 0 (0x0)   execution time : 0.012 s
    Press ENTER to continue.

    • Alex

      It may or may not. By commenting out the user-defined (deep) copy constructor, you’re forcing the program to default to the compiler provided (shallow) copy constructor.

      If the compiler elides the return value (which it apparently has in your result, since you’re only getting 4 outputs), then you’re right, the function isn’t used. But if the compiler doesn’t elide the result, then you’re changing the way the program executes by commenting out the function.

      • hi
        i use ubuntu 16.04 with latest version of g++
        ……………………….

        i put this simple code

        And got the same results

        …………………

        ………………..

        ………………………….

        Resource acquired
        Resource acquired
        Resource destroyed
        Resource destroyed

        Process returned 0 (0x0)   execution time : 0.046 s
        Press ENTER to continue.
        ……………………………..
        This means that this piece of code does not do anything

        • Alex

          I already answered this in my comment above. Your compiler is eliding the copy constructor, so in your case it isn’t doing anything. But other compilers may not elide the copy constructor, or they may not elide for debug builds (where optimizations are generally disabled). Please go back and re-read the section of http://www.learncpp.com/cpp-tutorial/911-the-copy-constructor/ regarding copy elision.

  • Lamont Peterson

    Alex,

    Under "Another example", the first "version" of the DynamicArray class on line 12, and in the second version on line 10, the order of the initializations for the constructor should be reversed, because m_array is defined before m_length.  In the current order, this results in compiler warnings.  Even though the code works fine, I generally prefer to eliminate all warnings, if I can.  In this case, it doesn’t change the functionality in any way, but it’s still nice to have -Wall and see no warnings in the compiler output.

    Also, the first version doesn’t seem to need the "#include <iostream>" line at the top.  I think that removing it would help make the example more consistent.

    • Lamont Peterson

      Alex,

      I’m consistently getting the similar results with both copy and move semantics with the 1,000,000 element DynamicArray<int>.  I also had to modify the output just a bit in order to get enough timer resolution to see that they weren’t identical, since they’re always about 15 microseconds to complete.  So, I decided to try it with 100 million elements (yeah, it’ll use a LOT more RAM, but shouldn’t cause any disk swapping).  With 100 million elements, I started to see a small, but statistically relevant separation between the two versions.  So, I whipped up a wrapper to run the test a million times, then give me some min/max/mean stats.

      I used the Timer class as you provided, and I created two Timer instances t1 & t2.  I reset t1 just before the iteration loop and reset t2 at the top of each trip through the loop.  t1 is used only to report the total time through, t2 is recorded into an element of a DynamicArray<double> (called dataPoints) at the very end of each loop.  Only after reporting on t1, does the code run through dataPoints to calculate the other results.

      Results were similar to this (on a Mac mini (mid 2011) w/2.3GHz Intel Core i5 CPU (dual core), 8GB 1067MHz DDR3 RAM):

      I also ran some shorter sets (10-50 iterations) where I spat out all of the data points and saw that the first iteration was always the slowest, the second was always faster than the first, but slower than the rest of the set’s consistent members.  Oddly, on short sets, iterations 3 and on were pretty consistent (but still mostly getting a little faster on each loop through), but the longer sets showed the third one longer than the second.  I ran 1,000 iterations and saw that the first two were larger, but it settled immediately on iteration 3.

      Obviously, we’re seeing some fun with L1, L2 & L3 cache vs. main memory and on longer iteration sets, it’s settled into one of the cores.  Also, I should probably take some time to code it to exclude the outliers (anything, say, 2 or 3 times the resulting average?) from the averaging calculations.

      This has been a fun bit of exercise, to work up better code to run the test millions of times and calculate some stats that one might get some meaning (pun intended) from.

      But I’m still a little curious as to why the results of copy vs. move semantics "benchmarking" (yeah, it’s not good enough, yet) on my system appear to be within 3-5% of each other.  On the longer sets, it was clear that move was (more or less) always faster, but by such a small margin?  Any thoughts?

      My compiler did elide the Auto_ptr3 example.

      If you like, I’ll share my code, but don’t judge me by the rough quickie testing bits 🙂 …  I think I’ll continue developing this "benchmarking" code a bit further, since such things are always useful to reuse.

      (Also, I forgot to mention line 27 of the last version of the DynamicArray class in my first comment, which is also initializing in the wrong order and would generate compiler warnings.)

      • Alex

        Modern compilers do a great job of optimizing code. The small delta you’re seeing between copy and move is probably a testament to the compiler’s optimization capabilities. You might see greater variance with more complex programs where the compiler isn’t able to optimize quite as well.

    • Alex

      All fixed. You just taught me something -- I had always assumed that variables would be initialized in the order defined in the member initialization list -- but it turns out, they’re initialized in the order in which they are declared in the class. In most cases, this doesn’t matter, but if the initialization of one member variable is dependent upon another, then ordering could matter.

  • Ashwin

    Hi Alex,

    In Auto_ptr4 class, I tried to invoke move ctor by creating like this:

    This should invoke move constructor right? I did this, dont see a move happening. It just invokes the default constructor. Please guide

    • Alex

      Your compiler is likely performing copy elision in this case (as an optimization). When I compile this in Visual Studio release mode, copy elision is performed. In debug mode, I see the move constructor get called, as optimizations tend to be disabled in debug mode.

  • Sihoo

    Alex,

    When you say,

    "So res is copy constructed into a temporary object",

    is ‘temporary object’ same as anonymous object you explained back in 8.14?

  • Michael

    Hi Alex,

    I have a question regarding the following-
    "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!"

    I thought that even if Auto_ptr5 was a const l-value reference, it can’t be converted to an r-value. So passing Auto-ptr5 by const l-value still wouldn’t work, correct?

    Let me know if I am getting something confused, thanks!

    • Alex

      Auto_ptr5 is just a class, it’s the instance of the class that’s an l-value or r-value. But yes, you can pass it by const l-value reference.

      Remember that a const l-value reference parameter can bind to an l-value, const l-value, or r-value argument.

  • Hardik

    Why can’t we define move constructors or move assignment with l-value references as their parameters?

  • KnowMore

    Instead of getting 6 outputs, I get Only 4;

    Output :-
    Resource Acquired (x2)
    Resource Destroyed (x2)

    When i performed debugging, i noticed that ‘resource acquired’  that gets printed due to the  the copy constructing of res into a temp. object was not done !
    Help Plz 🙂
    Thanks In Advance !

    • Alex

      I get 6 outputs in debug mode, and 4 in release mode using Visual Studio 2017. The difference is that in release mode, it’s likely the return value from generateResource() is elided.

  • C++ Learner

    copy constructor copies the whole object, but move constructor not all the object? is it right? can you explain the difference more clearly? 🙂

    • Alex

      The copy constructor and move constructor can do whatever you want them to. 🙂 But the way they are _intended_ to be used is for the copy constructor to make a full copy of the source object into the newly constructed object, and for the move constructor to “move” the contents of the source object into the newly constructed object. With dynamically allocated data, we generally do a move by shallow copying the source data into the destination, and then setting the source to nullptr.

  • 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