0

I have a ArrayWrapper class which implements both move and copy constructor.

I have 2 Questions.

  1. You can see that in my code, the copy constructor ArrayWrapper (ArrayWrapper& other) is exactly same as move constructor ArrayWrapper (ArrayWrapper&& other)!? Both works just the same and has the same elapsed time. (I commented out the original copy constructor) How can the copy and move constructor use the same code and still work?

  2. The code actually isn't hitting the move constructor (I suspect this is why my first question works), I put a comment in both functions, and the output does not show that the move constructor is being called, even though I am passing an rvalue reference ArrayWrapper d3(ArrayWrapper(10000000));. What am I doing wrong?

    #include <bits/stdc++.h>
    #include <chrono> 
    using namespace std;
    using namespace std::chrono; 
    class ArrayWrapper
    {
    public:
        ArrayWrapper ()
            : _p_vals( new int[ 64 ] )
            , _size( 64 )
        {
            cout<<"?"<<endl;
        }
        ArrayWrapper (int n)
            : _p_vals( new int[ n ] )
            , _size( n )
        {
        }
        ArrayWrapper (ArrayWrapper&& other)
            : _p_vals( other._p_vals  )
            , _size( other._size )
        {
            cout<<"Move"<<endl;
            other._p_vals = NULL;
            other._size = 0;
        }
    
        ArrayWrapper (ArrayWrapper& other)
            : _p_vals( other._p_vals  )
            , _size( other._size )
        {
            cout<<"Copy constructor"<<endl;
            other._p_vals = NULL;
            other._size = 0;
        }
        /*
        // copy constructor
        ArrayWrapper (const ArrayWrapper& other)
            : _p_vals( new int[ other._size  ] )
            , _size( other._size )
        {
            for ( int i = 0; i < _size; ++i )
            {
                _p_vals[ i ] = other._p_vals[ i ];
            }
        }
        */
        void generate(){
            for(int i=0; i<_size; i++){
                _p_vals[i] = i;
            }
        }
        ~ArrayWrapper ()
        {
            delete [] _p_vals;
        }
        void print(){
            for(int i=0; i<_size;i++){
                cout<<_p_vals[i]<<" ";
            }
            cout<<endl;
        }
    
    private:
        int *_p_vals;
        int _size;
    };
    
    int main(){
        auto start = high_resolution_clock::now();
        ArrayWrapper d(10000000);
        ArrayWrapper d2(d); //deep copy
        auto stop= high_resolution_clock::now();
        auto duration = duration_cast<microseconds>(stop - start); 
        cout << duration.count() << endl; 
    
        auto start2 = high_resolution_clock::now(); 
        ArrayWrapper d3(ArrayWrapper(10000000)); //shallow copy
        auto stop2 = high_resolution_clock::now(); 
        auto duration2 = duration_cast<microseconds>(stop2 - start2); 
        cout << duration2.count() << endl;
    
    }
    
Telenoobies
  • 938
  • 3
  • 16
  • 33
  • 3
    "I have 2 Questions" - Then you should *ask two questions*. Not bundle them up in one. – Jesper Juhl May 10 '19 at 16:40
  • 2
    `ArrayWrapper (ArrayWrapper& other)` is not a copy constructor. – 273K May 10 '19 at 16:41
  • 3
    Copy constructor should be `ArrayWrapper(const ArrayWrapper &other)` and should not be modifying `other`. – 1201ProgramAlarm May 10 '19 at 16:41
  • @S.M Syntactically it is, but semantically it is not. – NathanOliver May 10 '19 at 16:41
  • @S.M But what is the point of Rvalue reference if `ArrayWrapper (ArrayWrapper& other)` works the same way? In other words, why bother having `ArrayWrapper (ArrayWrapper&& other)` when you can just implement `ArrayWrapper (ArrayWrapper& other)` – Telenoobies May 10 '19 at 16:42
  • It does not work for r-values, temp values for example. – 273K May 10 '19 at 16:43
  • @Because it doesn't work the same way. An non-const lvalue reference vs. an rvalue reference have different potential usage contexts. – WhozCraig May 10 '19 at 16:43
  • @S.M If you run my example, you will see that `ArrayWrapper d3(ArrayWrapper(10000000));` has the same run time as `ArrayWrapper d(10000000); ArrayWrapper d2(d);` – Telenoobies May 10 '19 at 16:44
  • @Telenoobies Yes because you made the function bodies similar, they have similar run times. What's the issue with that? – Artyer May 10 '19 at 16:46
  • 1
    I don't understand why you think two functions that do the same thing should have different performance characteristics. – Lightness Races in Orbit May 10 '19 at 16:47
  • @Artyer Yes, I know the function body is the same, but if you can accomplish the same thing using `ArrayWrapper (ArrayWrapper& other)`, then why bother having `ArrayWrapper (ArrayWrapper&& other)` I thought he whole point of rrfer copy constructor is because lvalue copy constructor is slow. But if you can write the same way for lvalue constructor and rvalue constructor, then just write lvalue copy constructor the way you would write an rvalue constructor. – Telenoobies May 10 '19 at 16:47
  • @S.M.: "*is not a copy constructor.*" Yes, it is. Unfortunately. – Nicol Bolas May 10 '19 at 16:47
  • 1
    Your second question is a dupe of [this](https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) so you should just remove it. – NathanOliver May 10 '19 at 16:48
  • @Telenoobies: Then all of your copying would perform the effects of a move. So how would you be able to copy when you actually want to *copy*? Are you aware of the difference in effects between a copy and a move? – Nicol Bolas May 10 '19 at 16:50
  • @Telenoobies the point is not that you should implement them the same. The point is that when you know you are moving you can write something that's more efficient than just a copy and then you write that and it will be faster. – Jesper Juhl May 10 '19 at 16:51
  • @NicolBolas I suppose that is an argument for it, but i'm still not 100% convinced... Because you also have copy operator, maybe use copy constructor as a shallow copy and copy operator as a deep copy? – Telenoobies May 10 '19 at 16:53
  • 1
    You may want to read [What are move semantics?](https://stackoverflow.com/questions/3106110/what-are-move-semantics) – R Sahu May 10 '19 at 16:56
  • 2
    @Telenoobies *"maybe use copy constructor as a shallow copy"* - nothing is technically stopping you from doing that, but no self-respecting C++ programming on earth will want to maintain that code. The very point of a copy-ctor is to *make a copy*; not assume ownership. It's in the *name*. Assuming ownership has it's own set of operations (move-ctor and move-assignment). – WhozCraig May 10 '19 at 17:00
  • Thank you guys, I think my questions are answered. – Telenoobies May 10 '19 at 17:16

2 Answers2

3

Both works just the same ... How can the copy and move constructor use the same code and still work?

The body of the function does the same thing, so it works the same way.

Regarding your implication that the copy constructor "works": It is highly non-idiomatic for a copy constructor to modify the operand. Many would agree that it "works" in a wrong way.

P.S. There used to be a smart pointer in the standard library called std::auto_ptr which took ownership of pointer passed to the copy constructor. It was an attempt to implement unique pointer before move semantics existed. There is no longer any reason to do that, since we do have move constructors. std::auto_ptr was removed from the standard for a good reason.

The code actually isn't hitting the move constructor ... What am I doing wrong?

You're initialising from a prvalue expression of the same type. d3 is constructed directly using the initialiser expression of ArrayWrapper(10000000) without any temporary object. In other words, only ArrayWrapper (int n) is used; not the move constructor.

Prior to C++17, there would be a temporary object which would be moved from as far as the abstract machine is concerned, but even then, the move can be elided, which means the same as above (no temporary, no move) except it is an optimisation that is merely allowed, and not mandated by the standard.

What you're doing wrong is expecting there to be a move, or that the move is guaranteed to have side effects.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • "` d3` is constructed directly from the expression `ArrayWrapper(10000000)`" Which means d3 should be called using move constructor, since `ArrayWrapper(10000000)` is a temp obj which is an rvalue. – Telenoobies May 10 '19 at 16:59
  • @Telenoobies I changed the wording. Is it more understandable now? – eerorika May 10 '19 at 17:05
  • 1
    @Telenoobies See Copy elision here: https://en.cppreference.com/w/cpp/language/copy_elision No temporary is created. – Richard Critten May 10 '19 at 17:08
  • @RichardCritten Can you elaborate on what does it mean no temporary is created? You mean `ArrayWrapper d3(ArrayWrapper(10000000))` only calls the constructor once, basically doing something like `ArrayWrapper d3(10000000)`? – Telenoobies May 10 '19 at 17:13
  • @Telenoobies yes. It means exactly that. – eerorika May 10 '19 at 17:13
  • Thank you guys, I think my questions are answered. – Telenoobies May 10 '19 at 17:16
  • @Telenoobies Yes. The compiler sees that it can just create the object directly *in* the destination and does not actually need to do neither a copy nor a move, nor does it need to create any temporaries, so it does exactly that (elides the copy/move). This it is allowed to do by the language standard. – Jesper Juhl May 10 '19 at 17:20
2

You can see that in my code, the copy constructor ArrayWrapper (ArrayWrapper& other) is exactly same as move constructor ArrayWrapper (ArrayWrapper&& other)!? Both works just the same and has the same elapsed time. (I commented out the original copy constructor) How can the copy and move constructor use the same code and still work?

The only time the copy constructor and move constructor can be exactly the same is when you are dealing with types that are only copyable (or to move them is to get a copy). This is not what you have. In

ArrayWrapper (ArrayWrapper& other)
    : _p_vals( other._p_vals  )
    , _size( other._size )
{
    cout<<"Copy constructor"<<endl;
    other._p_vals = NULL;
    other._size = 0;
}

You basically have an lvalue move constructor. If you were to print the size of d after ArrayWrapper d2(d); you would get 0 since you steal its data. This isn't making a copy and would really confuse a lot of programmers since it does something totally unexpected. You need to use the code you have in ArrayWrapper (const ArrayWrapper& other) if you really want a copy to be made.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • Ok, Thanks!! I understood the first part I think. basically, it's bad programming on my part if I use a copy constructor and change the original object. Thank you, you addressed my first point, but what about the second point? Why isn't the move constructor being hit? Any ideas? – Telenoobies May 10 '19 at 16:57
  • @Telenoobies Give [this](https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) a read. It will explain why no copy/move happens. – NathanOliver May 10 '19 at 16:59
  • It just says there is a thing called elision, and it's to improve performance. But I'm still not sure why it applies to my example. – Telenoobies May 10 '19 at 17:11
  • Thank you Nathan, I think my questions are answered. – Telenoobies May 10 '19 at 17:16