1

I read the following article about rvalue references http://thbecker.net/articles/rvalue_references/section_01.html

But there are some things I did not understand.

This is the code i used:

#include <iostream>

template <typename T>
class PointerHolder
{
public:
    // 1
    explicit PointerHolder(T* t) : ptr(t) 
    {
        std::cout << "default constructor" << std::endl;
    }

    // 2
    PointerHolder(const PointerHolder& lhs) : ptr(new T(*(lhs.ptr)))
    {
        std::cout << "copy constructor (lvalue reference)" << std::endl;
    }

    // 3
    PointerHolder(PointerHolder&& rhs) : ptr(rhs.ptr)
    {
        rhs.ptr = nullptr;
        std::cout << "copy constructor (rvalue reference)" << std::endl;
    }   

    // 4
    PointerHolder& operator=(const PointerHolder& lhs)
    {
        std::cout << "copy operator (lvalue reference)" << std::endl;
        delete ptr;
        ptr = new T(*(lhs.ptr));
        return *this;
    }

    // 5
    PointerHolder& operator=(PointerHolder&& rhs)
    {
        std::cout << "copy operator (rvalue reference)" << std::endl;
        std::swap(ptr, rhs.ptr);
            return *this;
    }

    ~PointerHolder()
    {
        delete ptr;
    }
private:
    T* ptr;
};

PointerHolder<int> getIntPtrHolder(int i)
{
    auto returnValue = PointerHolder<int>(new int(i));
    return returnValue;
}

If I comment constructors 2 and 3, the compiler says :

error: use of deleted function ‘constexpr PointerHolder<int>::PointerHolder(const PointerHolder<int>&)’
  auto returnValue = PointerHolder<int>(new int(i));
                                                  ^
../src/rvalue-references/move.cpp:4:7: note: ‘constexpr PointerHolder<int>::PointerHolder(const PointerHolder<int>&)’ is implicitly declared as deleted because ‘PointerHolder<int>’ declares a move constructor or move assignment operator

But If I uncomment any one of the two, it compiles and the execution yields the following :

default constructor

So these are my questions :

  • When the constructors 2 and 3 where commented, it tried to call the constructor 2. Why ? I would expect it to call the constructor 1, which is what it did when i uncommented them !
  • Regarding the error : I declared a "move" copy operator, which means that my object can be "move" copied from a rvalue reference. But why does it implicitly delete my normal copy constructor ? And if so why does it allow me to "undelete" it by defining it explicitly and use it ?
mleandro
  • 21
  • 3
  • possible duplicate of [Why user-defined move-constructor disables the implicit copy-constructor?](http://stackoverflow.com/questions/11255027/why-user-defined-move-constructor-disables-the-implicit-copy-constructor) – Shoe Apr 18 '14 at 08:30
  • or [this](http://stackoverflow.com/q/5619292/493122) – Shoe Apr 18 '14 at 08:31
  • 2
    Note that in your copy constructor by rvalue reference, you forget to reset `rhs.ptr` to `nullptr`, and so you will have double `delete`. – Jarod42 Apr 18 '14 at 08:48
  • Yes thank you. I also forgot to return *this in the move-assignment operator. I will now edit my original post with these changes. – mleandro Apr 18 '14 at 08:53

3 Answers3

2

When the constructors 2 and 3 where commented, it tried to call the constructor 2. Why ?

Because your declaration initialises returnValue from a temporary object - that temporary needs to be movable or copyable, using a move or copy constructor. When you comment these out, and inhibit their implicit generation by declaring a move-assignment operator, they are not available, so the initialisation is not allowed.

The actual move or copy should be elided, which is why you just see "default constructor" when you uncomment them. But even when elided, the appropriate constructor must be available.

why does it implicitly delete my normal copy constructor ?

Usually, if your class has funky move semantics, then the default copy semantics will be wrong. For example, it might copy a pointer to an object which is only supposed to be pointed to by a single instance of your class; which might in turn lead to double deletion or other errors. (In fact, your move constructor does exactly this, since you forgot to nullify the argument's pointer).

It's safer to delete the copy functions, and leave you to implement them correctly if you need them, than to generate functions which will almost certainly cause errors.

And if so why does it allow me to "undelete" it by defining it explicitly and use it ?

Because you often want to implement copy semantics as well as move semantics.

Note that it's more conventional to call 3 a "move constructor" and 5 a "move-assignment operator", since they move rather than copy their argument.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • This is terribly off-topic and silly but.. could you throw an eye on my answer to see if I got the mechanism correctly? I recognize that you've got way more experience than me and I wouldn't mind a downvote if I got something wrong. – Marco A. Apr 18 '14 at 08:46
  • Thank you! I always find something new on this website – Marco A. Apr 18 '14 at 08:51
  • Okay I think I understood ! Thank you all for your answers. – mleandro Apr 18 '14 at 08:59
2

Because you're deleting the copy constructor and the line

auto returnValue = PointerHolder<int>(new int(i));

isn't a real assignment, it invokes a copy constructor to build the object. One of the two copy constructors (either by reference or by rvalue) needs to be available in order to succeed in initializing the object from that temporary. If you comment those both out, no luck in doing that.

What happens if everything is available? Why aren't those called?

This is a mechanism called "copy elision", basically by the time everything would be properly available to "initialize" returnValue with a copy-constructor, the compiler's being a smartboy and realizing:

"oh, I could just initialize returnValue like this"

PointerHolder<int> returnValue(new int(i));

and this is exactly what happens when everything is available.


As for why the move constructor seems to overcome the implicit copy-constructor, I can't find a better explanation than this: https://stackoverflow.com/a/11255258/1938163

Community
  • 1
  • 1
Marco A.
  • 43,032
  • 26
  • 132
  • 246
0

You need a copy or move constructor to construct your return value.

If you get rid of all copy/move constructors and all assignment operators (use default generated constructors/operators) the code will compile, but fail miserably due to multiple deletions of the member ptr.

If you keep the copy/move constructors and assignment operators you might not see any invocation of a constructor, due to copy elision (return value optimization).

If you disable copy elision (g++: -fno-elide-constructors) the code will fail again due to multiple deletions of the member ptr.

If you correct the move-constructor:

// 3
PointerHolder(PointerHolder&& rhs) : ptr(0)
{
    std::swap(ptr, rhs.ptr);
    std::cout << "copy constructor (rvalue reference)" << std::endl;
}

And compile with disabled copy elision the result might be:

default constructor
copy constructor (rvalue reference)
copy constructor (rvalue reference)
copy constructor (rvalue reference)