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 ownership of the resources from one object to another (which is much less expensive than making a copy).

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. We’ll talk about why this happens below.
3) Res goes out of scope. Because res no longer manages a pointer (it was moved to the temporary), nothing interesting happens here.
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), nothing interesting happens here either.
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 both 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?

The answer is simple: When “a” goes out of scope, a’s destructor will be called, and a.m_ptr will be deleted. If at that point, a.m_ptr is still pointing to the same object as m_ptr, then m_ptr will be left as a dangling pointer. When the object containing m_ptr eventually gets used (or destroyed), we’ll get undefined behavior.

Additionally, in the next lesson we’ll see cases where “a” can be an l-value. In such a case, “a” wouldn’t be destroyed immediately, and could be queried further before its lifetime ends.

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. 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.

Although the compiler can move l-value return values, in some cases it may be able to do even better by simply eliding the copy altogether (which avoids the need to make a copy or do a move at all). In such a case, neither the copy constructor nor move constructor would be called.

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

108 comments to 15.3 — Move constructors and move assignment

  • Louis Cloete

    @Alex, I am no expert on benchmarking software, but I think you should rather run both programs at the end like 100 times. I heard somewhere (and it makes sense) that you should run your programs a lot of times to ensure that you get time values large enough your instrumentation can detect and to combat statistical variance effects.

    • Alex

      How many times you should run something varies on how much variability you might expect and how much confidence you require in the result.

      For simple timings like this, my general take is that if take 3 timings of each, alternated (e.g. A B A B A B), and all the A's and all the B's cluster, then you're probably on solid ground as far as the comparison goes.

      It's certainly not scientifically rigorous, but most of the time we don't require that level of precision and the results often aren't close enough that variance would matter.

      But I'm no expert either.

  • topherno

    Hey Alex,

    Forgive me if this is a stupid question, but isn't move semantics only applicable/useful to the pointer to (i.e. owner of) a large resource, and not the resource itself? In other words, there's no such thing as moving the resource itself right? In the above examples, the dynamically allocated Resource is never moved at all, instead the address of its owner (i.e. its pointer) is the thing being reassigned (moved).
    So I guess the stupid part of the question is: If we want to actually move the resource itself (assuming we can), that corresponds to actually moving its location in memory, and that always constitutes a deep copy, right? (Or more precisely, a "deep move", if that makes sense).

    • Alex

      Right. Moving really means changing owners. If we want to change the location in memory, that requires a deep copy, which is a copy, not a move.

      • topherno

        Fantastic, that makes sense. Thanks for the quick reply!

      • topherno

        I have another question about this: I see a lot of code where STL containers are EXPLICITLY moved around via std::move, including in the next section of this tutorial (15.4). Does this mean that internally, the STL containers are implemented as "owners" of their elements, i.e. pointers to their elements? I read something about the elements of e.g. vector being allocated on the heap by default, while the vector object/identifier itself is allocated on stack. So for example, if we move a vector (when e.g. passing it to a function), it won't actually move the elements (which are the "resources"), but just change the address of the vector object or something like that?

        And I imagine the same might be true for the examples in 15.4, where an std::string is passed via std::move? In this case, the little underlying char array of an std::string is the resource, and the std::string object is the owner?

        • Hi!

          Every container that can change its size at run-time (eg. @std::string, @std::vector, but not @std::array) has to allocate its internal array dynamically (ie. on the heap).
          So the container only stores some meta information, eg. length, and a pointer to the data. This allows moving a container without moving the individual elements.

  • jerron

    According to what I read, the first DynamicArray sample used Copy and took 0.00825559 seconds while the second sample used Move and took 0.0056 seconds.  -- Isn't the Move version took longer time and ran slower?

  • Danty Wong

    In the last example, where exactly do the copy and move take place?

    Also, since dbl is an I-value and is being returned by value, wouldn't some compiler opt to do a move instead a copy?
    If so, the comparison in the final example don't seem to be very fair (since the copy version could be even slower).

    I tried to delete the move constructor and move assignment to ensure the use of copy construct and copy assignment. But I get an error instead.
    So obviously the compiler tried to do a move when returning from @cloneArrayAndDouble instead of a copy, even though I deleted the move assignment operator.
    Is there a better way to force a copy (I want to see how much slower the copy version can actually be)

    DynamicArrayCopy.hpp

    Seems like my compiler is trying to to do a move assign, even thought what I really want is to do a copy assign.

    • Alex

      In the last example, the compiler is doing a move assignment when the return value for cloneArrayAndDouble() is assigned back to arr.

      You should be able to delete the move constructor and move assignment -- see lesson 9.13 for an example of how to do so.

      • Anthony

        I had the same problem as Danty Wong. i.e. delete-ing the move constructor and move assignment operator only made VC++'s compiler error out - "attempting to reference a deleted function". The only way I could get it to work was to comment out the move constructor and move assignment operator entirely.

        Brute force :)

        I did look back at lesson 9.13, but I was already doing it perfectly, so is there a way around this..?

        • Alex

          Sorry, I think I misunderstood what Danty was asking.

          If you delete the move constructor/assignment, the compiler will still try to use it when it's appropriate, but it will then throw a compile error (because you've asked it to).

          If you want the compiler to do copies instead of moves, don't delete the move constructor/assignment. Instead, leave them undefined, but explicitly define the copy versions. That way the copy versions will be the "best match", and thus get used.

  • Asgar

    Hi Alex,

    There is one thing I do not understand here:

    The return statement is returning res, which is an l-value. But, the move constructor takes an r-value as an argument. It is only after the move constructor completes executing that a temporary object is created which is an r-value. But to begin with, where does the compiler find an r-value out of res?

    • Asgar

      Hi. Please remove my original comment since I didn't read this section where you say, "Automatic l-values returned by value may be moved instead of copied."

      I have one concern though! It seems, we are at the mercy of the compiler. We just come up with a move constructor and move assignment and hope for the best while letting the compiler decide whether to choose move or copy?

      In general, if our program design requires us to write our own copy constructor and copy assignment operator, should we also provide our own move constructor and move assignment operator just to be on the safe side?

      • Alex

        The compiler makes many decisions on our behalf. The difference between an unoptimized and optimized program may be huge, and it's the ability of the compiler to optimize that makes the difference.

        There are two "rules" that make sense here:
        1) The "Rule of zero" says that any class that has a custom destructor, copy or move constructor, or copy or move assignment should deal with ownership and nothing else (e.g. these should generally be containers or smart pointers, not value-carrying classes).
        2) The "Rule of five" says that if move semantics are desirable, you should always define the full complement of destructor, copy and move constructor, copy and move assignment.

        So, in short, define move operators only when necessary and when the class would benefit from doing so (e.g. because it deals with ownership). If the class doesn't benefit from it, or if the class members already know how to handle moves, just use the default provided move constructor and assignment.

        • Asgar

          About the last line of the last paragraph: "If the class doesn't benefit from it..... just use the default provided move constructor and assignment.."

          But, don't the default provided move constructor and assignment act the same as the default provided copy constructor and assignment, respectively? You wrote in the tutorial that they just do shallow-copy. Can't we then just delete the default move functions, since they don't really move things?

          • Alex

            https://en.cppreference.com/w/cpp/language/move_constructor says this:
            "For non-union class types (class and struct), the move constructor performs full member-wise move of the object's bases and non-static members, in their initialization order"

            So basically, members that can be moved will be moved. And those that can't will be copied.

  • Anthony

    Thanks for the articles, C++11 smart pointers and move semantics is very well explained!

    But sometimes smart pointers can hide bad underlying design and also I think there could be worst performance when using smart pointers instead to "naked" pointers. I mean that smart pointers are very good thing but still there is a price. For example if you have too many calls using the unique_ptr access operator is it the same as when directly make calls using a "naked" pointer? Also I think that std containers can have issues, for example if you have std::map from unique_ptrs, it is still a separate objects, what will be insert/find performance compared to the "naked" pointers. Lets hope this will be further explained.

  • Brian

    Hi Alex,

    Consider these 2 lines:
    m_ptr = a.m_ptr;
    a.m_ptr = nullptr;

    Since it's shallow copying. How is m_ptr not point to nullptr when you set a.m_ptr to nullptr?

  • Fede

    Hi Alex,
    thank you for your wonderful tutorial! I have a question about this sentence:

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

    But in this specific example is very important, the temporary r-value will be destroyed and the destructor will be called and will free the resource we have just moved.

    Is it correct?

    • Alex

      When I wrote that paragraph, I was thinking about the case where "a" could be an l-value, not an r-value. But you're right, we absolutely don't want to leave a dangling pointer either way, and that alone necessitates setting a.m_ptr to nullptr.

      I've updated the lesson accordingly. Thanks for the great feedback.

  • TC

    Hi Alex,

    I add a debug function to Auto_ptr3 so that it will help me to illustrate my questions more clear.

    As you mention in the 2) point where it says: Since our copy constructor does a deep copy, a new Resource is allocated here, which causes the second “Resource acquired”. However, I cannot get the copy constructor being involved as I run the program. You can examine from my program result. I wonder that why this is the case?

    I have also another 5 questions that are preceded with //---> symbol to indicate where are they. Some of them may seem to ask the same question.

    Truly appreciated your help.

    The result from running on Mac.
    1 main: begins
    2 Auto_ptr3: Default constructor is called!
    3 main: before calling generatedResource()
    4 generatedResource: begins
    5 Resource: acquired
    6 Auto_ptr3: Default constructor is called!
    7 generatedResource: ends
    8 Auto_ptr3: Copy assignment is called
    9 Resource: acquired
    10 Auto_ptr3: after new T(inside copy assignment operator)
    11 Resource: destroyed
    12 main: ends
    13 Resource: destroyed
    Program ended with exit code: 0

    • Hi TC!

      Q1,5: You're not passing an argument, there's no reason for the copy constructor to be called
      Q2: Constructor elision/return value optimization. If you're using a compiler with a gcc interface you can use "-fno-elide-constructors".
      Q3: @a is @res from @generateResource. Whether or not it's destroyed depends on the call to @operator=. In this case, it will be destroyed.
      Q4: @res is an l-value.

      • TC

        Hi Nas,

        As always, your answers are very succinct and concise. A follow up question just want to get things clarified a bit more. Alex you mentioned that "Since our copy constructor does a deep copy,
        a new Resource is allocated here, ..." Does this means that either the compiler you used did not have "constructor elision/return value optimization" and that's why the copy constructor was called or you were referring to copy assignment operator that does a deep copy instead?

  • Ramkrushna

    Hi Alex,

    I have one doubt.
    If I define my smart class as per Auto_5,then
    -If I pass my smart class object by value to a function to read data and do some action[depending on values I want to perform some action].[since it will be l-value, I have to define copy constructor as per Auto_3 as well]
    -and after calling that function, I am using same object.
    So, it will also crash...,right?
    How to avoid this ?

    • Alex

      No, it should be okay. Auto_ptr3's copy constructor does a deep copy, so when you pass your Auto_ptr by value to a function, it would make a deep copy of the object. Then when that Auto_ptr parameter went out of scope, the copy would be destroyed.

      But the better answer is just to disable copying altogether so you can't pass Auto_ptr by value.

  • smsouls

    what's the difference between m_ptr = a.m_ptr; and *m_ptr = *a.m_ptr;

Leave a Comment

Put all code inside code tags: [code]your code here[/code]