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

64 comments to 15.3 — Move constructors and move assignment

  • Anil Jaiswal

    Hi Alex,

    I have doubt regarding unique_ptr implementaion in Auto_ptr5 where copying is disabled.

    In that piece of code, in case of assignment operator using move sementics.
    First we delete the m_ptr which is fine then transfer the memory from a.ptr to ptr(m_ptr = a.m_ptr).
    My doubt is with creating new memory how we are able to copy it.

    Please clarify my doubt.

    • Alex

      This doesn't create new memory or copy it. All we're doing is making m_ptr point at the same resource that a.m_ptr points at, and then making a.m_ptr point at null. This completes the transfer, but really all we've done is updated a couple of pointers.

  • SirSimonMilligan

    Wow, thank you for this tutorial.  I am really enjoying it.  

    I think I found a spelling error in 3 places in 15.3:  

    Should that read:  

    Pretty trivial error, unless it isn't an error and I'm just missing something. Either way it was confusing.

  • Topherno

    Hi again Alex 🙂 Sorry for my many questions recently, I know you're busy. I really hope you can help me with these two though, I've been reading these sections over and over again and I'm still stumped!

    1)
    First, please let me know if I'm understanding these fundamental concepts correctly, I'm not sure I am:
    In the context of passing and returning to/from functions, copy or move semantics are only ever invoked if we're passing/returning by value. I.e. if we pass by ref, we're not creating a new object, the function parameter instead merely becomes a reference to the passed in object, which has nothing to do with object construction. Similar idea with returning by ref.

    Now assuming the above is true, if we pass/return by value, copy semantics are invoked if the argument is an l-value, and move semantics if the argument is an r-value (assuming we've defined the needed move constructor/move assignment operator). In other words, as stated on http://thbecker.net/articles/rvalue_references/section_03.html, "lvalues prefer old-style lvalue references, whereas rvalues prefer the new rvalue references."

    This would seem to contradict one of your answers to another question:
    "We don't want to pass Auto_ptr5 by value, both because it invokes copy semantics..."
    Couldn't we pass a temporary object (r-value) by value to a function and it would invoke the move constructor? (I.e. not copy semantics.)

    2)
    I still don't really understand why copy constructors and copy assignment operators that implement move semantics (like Auto_ptr2 in 15.1) are insufficient and inferior compared to move constructors and move assignment operators. Here's the copy ctor and copy assignment operator of Auto_ptr2 from 15.1:

    It seems the only disadvantage the above functions have compared to their move counterparts is that they can't accept r-value arguments (temporary variables). Further, why do we need r-value refs when we can instead set the parameters of the above functions to be const l-value ref so they can also accept r-value arguments? Sure, then you wouldn't be able to set the argument (the source) to nullptr since the parameter is const, but I don't see why this is a problem, even after reading section 15.4, because why does an object necessarily need to be set back to its zero state in order to be reused? You can overwrite its fields and thus reuse it regardless of whether the fields are in their zero state or still contain the stolen value.

    EDIT: Is not being able to set the argument (the source) to nullptr a problem precisely because it can lead to the "critical flaw" of Auto_ptr1 in 15.1, i.e. the problem of shallow copies and dangling pointers?

    • Alex

      1) Generally correct. My answer in the comments section was incorrect (I meant move semantics, not copy semantics), and I've updated it. Thanks for pointing out the error.
      2) The two best arguments I have for why implementing move semantics through copy constructor and assignment is bad are:
      a) It can lead to unexpected results, like where you say res1 = res2; and res2 is changed. That's counterintuitive, and can lead to errors.
      b) It doesn't work with r-value arguments. While you can use a const l-value reference to bind to an r-value, you can't set the parameter to null. This is important, otherwise you're not doing a move, you're making a copy (probably a shallow one). This means you'd end up with two pointers pointed at the same thing, and when the second went out of scope and tried to delete the object it was managing, it would crash your program. So yes, your edit is correct.

      • Topherno

        So before r-value refs were created, how did one construct an object from an anonymous object (r-value) or assign an anonymous object to an already existing object? For example, how did one pass/return an anon object to/from a function by value? I was looking over section 8.14 again, and it seems like such operations with anonymous objects are quite useful and elegant sometimes, yet were they not possible pre r-value refs?

        Thanks very much for your above answer, very much appreciated! I can't say it enough: this tutorial is absolutely outstanding, and I and I'm sure many others are super grateful it exists 🙂

        • Alex

          > So before r-value refs were created, how did one construct an object from an anonymous object (r-value)

          No different than ever: SomeObject obj(std::string("foo"));

          Prior to move semantics, objects could be passed by const l-value reference and deep copied.

          Same with return values: previously, you'd return by value and have the class make a deep copy (or hope the copy got elided by the compiler). With move semantics, we can avoid the deep copy and subsequent destruction of the original.

  • Rahul

    what is meant by non-event?

    • Alex

      In this context, a function that doesn't end up doing anything useful or interesting. I've updated the article to remove this phrasing because it may not be obvious to non-native speakers.

  • Kyaa~st

    Ah, I wanted to say a useless thing! In your example it's almost 50% (~47.4%) faster which makes it better example than almost 33% slower.
    And, as usual, thanks for your tutorial!

  • kdart

    At the end of the function:

    res goes out of scope. So the Auto_ptr4<Resource> destructor is called.  But since the Resource pointer res is managing is nullptr, does the compiler then omit the delete m_ptr statement in the destructor?  And so the Resource class destructor is never called?  Or why isn't the Resource destructor called?

    • nascardriver

      Hi kdart!

      > res goes out of scope
      Correct

      > Auto_ptr4<Resource> destructor is called
      Correct

      > the Resource pointer res is managing is nullptr
      No, @res->m_ptr is pointing to the the resource creating by 'new Resource'

      Here's the flow (Without compiler optimization):
      * Create a new @Resource
      * Create a new @Auto_ptr4 called 'res' and call @Auto_ptr4::Auto_ptr4(T*), pointing @m_ptr to the new resource
      * Create a copy of @res by calling @Auto_ptr4::Auto_ptr4(const Auto_ptr4 &). This will copy the resource too.
      * Call @res->~Auto_ptr4, deleting the resource
      * Return the copy of @res

      Note: deleting a nullptr has no effect

      • kdart

        > * Create a copy of @res by calling @Auto_ptr4::Auto_ptr4(const Auto_ptr4 &). This > will copy the resource too.
        > * Call @res->~Auto_ptr4, deleting the resource
        > * Return the copy of @res

        But Auto_ptr4 has a move constructor defined.  So at the end of generateResource(), wouldn't a temporary Auto_ptr4 object be created and res->m_ptr be moved to temporary->m_ptr. And res->m_ptr be set to nullptr by the compiler.  So when res goes out of scope, and res->~Auto_ptr4 is called, does the compiler not delete res->m_ptr because it is nullptr, and that's why the Resource destructor is never called?

        • nascardriver

          Sorry, I didn't notice we were in the move lesson.

          You're right. But it's not your compiler doing the work, it's your OS/CPU. The compiler only creates the binary.

  • rachid

    Hi,
    In copy constructor and copy assignment, is this correct?

    • nascardriver

      Hi Rachid!

      Yes it is.

      • rachid

        Thank you @nascardriver

        Applied to Auto_ptr3 example, the output is:
        Resource acquired
        Resource destroyed
        Resource destroyed

        No memory leak nor errors detected under valgrind.

        • nascardriver

          This is happening, because in the original Auto_ptr3 only the default constructor is used to create a new T.

          Your code invokes to copy constructor of T

          The copy constructor isn't explicitly defined in @Resource. If we add it the output is equivalent to the original.

  • Nur

    Hello Alex,
    I have a question on your quote "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)".

    I made some researches on above your quotes. See, even copy constructor and move constructor are elided automatic objects returned from a function by value can be moved.
    See below program. Compile it your compiler.

    class Sample{
        int *m_ptr;
        public:    
        Sample(int *ptr=nullptr)
        :m_ptr(ptr)
        {
            std::cout<<"Constructor: "<<this<<'\n';
        }
        ~Sample()
        {
            std::cout<<"destructor: "<<this<<'\n';
            delete m_ptr;
        }
        Sample(const Sample &sp)
        {
            m_ptr=new int;
            *m_ptr=*sp.m_ptr;
        }
        Sample(Sample &&sp)
        {
            m_ptr=new int;
            *m_ptr=*sp.m_ptr;
        }
    };

    Sample getSample()
    {
        Sample s;
        return s;
    }
    int main()
    {
        Sample smain=getSample();
        std::cout<<"smain:"<<&smain<<'\n';
        
        return 0;
    }
    Neither copy constructor nor move constructor is called, i.e. Copy construtor and move constructor are elided, Even though, both objects's address are the same which are local of different functions( getSample() and main()).If they have the same address, it means the object "s" is moved to "smain" object.

    I read this on Wikipedia:

    it is  not guaranteed that a copy constructor will be called in all these cases which has to be called, because the C++ Standard allows the compiler to optimize the copy away in certain cases, one example being the return value optimization (sometimes referred to as RVO).

    In general, the C++ standard allows a compiler to perform any optimization, provided the resulting executable exhibits the same observable behaviour as if (i.e. pretending) all the requirements of the standard have been fulfilled.

    Conclusion is made that even copy constructor and move constructor are elided, it can be moved.
    So, How can we understand "if the copy isn't elided altogether" which is brackets in your quotes. May be it has a different meaning. Please could you shed light on this problem.  

    Thanks in advance.

    • So I expected this code snippet to definitely hit the move constructor but that doesn't seem to be happening. Any ideas as why not, or what I might be missing here?

      class Something {
      private:
          int m_value;
      public:
          Something(int value=0): m_value(value) {}
          
          Something(const Something &s): m_value(s.m_value) {
              cout << "Copy ctor called" << endl;
          }
          
          Something(Something &&s): m_value(s.m_value) {
              cout << "Move ctor called" << endl;
          }
          
          Something& operator=(const Something &s) {
              cout << "Copy = called" << endl;
              return *this;
          }
          
          Something& operator=(Something &&s) {
              cout << "Move = called" << endl;
              return *this;
          }
          
          friend ostream& operator<<(ostream& out, const Something &s) {
              out << s.m_value;
              return out;
          }
          
          int getValue() { return m_value; }
      };

      Something getSomething() {
          Something s;
          return s;
      }

      int main(int argc, const char * argv[]) {
          cout << getSomething();
          return 0;
      }

      • nascardriver

        Hi Siddharth!

        Your compiler is using return value optimization to speed up your program.
        When disabling return value optimization the move constructor will indeed be called.
        Output (with added default constructor message):

        Don't use 'using namespace'.
        Initialize your variables.
        Use CODE tags.

        • So I understand that the compiler can perform RVO, but looking at the output that my compiler generated for the code snippet below. I'm failing to understand how the resource is being moved?

          Output:

          Resource acquired
          ctor called
          in loop100
          100
          dtor called
          Resource destroyed

          1. Why are the ctor and dtor only being printed once? The first time that the ctor gets executed is for the `res` in the generateResource() function. I assume the dtor is for that as well.
          2. Why isn't there any ctor/dtor message being printed for the `mainRes` variable?
          3. How did the resource get moved to the `mainRes` variable?
          4. Given that in alot of cases the move/copy ctor can be elided. How do we ensure that some side effects which were being introduced within these are still executed ? Do we extract these out into a separate function and have all the ctors (normal/move/copy) call that function from their respective blocks?

          PS: how to add the code tags? I even tried ``` ``` and <code></code> but no luck

          • nascardriver

            I can't speak for your compiler, this is what my compiler (g++ 8.0.1) did:

            @generateResource and some other functions will get a pointer to an Auto_ptr4<Resource> as parameter. This way they won't have to create a totally new resource but instead can create the resource at a space in memory that has already been reserved to hold a resource.

            So, to your questions:
            1. There is not 'res' in @generateResource, there's only @mainres. So the messages you're seeing are actually the construction and destruction of @mainres.
            2. There is
            3. It didn't need to be moved because there never was another resource, it was directly created in @mainres.
            4. Depends on the situation. Is this case the only way to get the expected output is disabling RVO I'm guessing. Don't rely on constructors/destructors being called in an expected way. Handling allocation/deallocation in there is fine, but relying on them to do actual work that affects the programs behavior is risky.

    • Alex

      I think this is a phrasing issue. I was trying to indicate that a l-value return value can be copied, moved, or elided altogether. I'll update the wording.

  • Ivan

    Auto_ptr3 code for me is printing:
    Resource acquired
    Resource destroyed
    Resource destroyed

    Any clue why?

    • Alex

      No idea. At the very least, the number of acquired and destroyed lines should match, otherwise it's trying to destroy something that was never created.

  • Urio

    I have to make a suggestion here, add an asterisk or something to the part where you explain move constructors and assignments at step 2), noting that it will be explained bellow.
    I had a meltdown while trying to understand why was res returned using a move constructor and not a copy one (since it was an l-value), and spent like 15 minutes in that state (I don't like to go forward if I don't understand everything till that point), banging my head, giving up at the end & feeling like nothing makes sense anymore, only to find out that you explained it 3 paragraphs bellow.

    Beyond that, I have only words of praise. Great job as always.

  • 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, because it will invokes move semantics, that will cause a transfer of ownership to the function parameter. 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 all code inside code tags: [code]your code here[/code]