Bluerain
Bluerain

Reputation: 9

Copy constructor is called instead of move constructor - why?

I have this code, taken from here by the way http://www.cplusplus.com/doc/tutorial/classes2/

// move constructor/assignment
#include <iostream>
#include <string>
#include <utility>
using namespace std;

class Example6
{
    string* ptr;
public:
    Example6(const string& str) :
            ptr(new string(str))
    {
        cout << "DONT   MOVE " << '\n';
    }
    ~Example6()
    {
        delete ptr;
    }
// move constructor
    Example6(Example6&& x) :
            ptr(x.ptr)
    {
        cout << "MOVE " << '\n';
        x.ptr = nullptr;
    }
// move assignment
    Example6& operator=(Example6&& x)
    {
        delete ptr;
        ptr = x.ptr;
        x.ptr = nullptr;
        return *this;
    }
// access content:
    const string& content() const
    {
        return *ptr;
    }
// addition:
    Example6 operator+(const Example6& rhs)
    {
        return Example6(content() + rhs.content());
    }
};

int main()
{
    Example6 foo("Exam");
    Example6 bar = Example6("ple"); // move-construction

    foo = foo + bar; // move-assignment

    cout << "foo's content: " << foo.content() << '\n';
    return 0;
}

I only added output in constructor to see which is being called. To my surprise it is always the first one, copy constructor. Why does it happen? I did some research and found some info about elision. Is it somehow possible to prevent it and always call move constructor?

Also, as a side note, as I said this code is from cplusplus.com. However, I read about move semantics in some other places and I wonder if this move constructor here is done right. Shouldn't it call

  ptr(move(x.ptr))

instead of just

ptr(x.ptr)

The way I understand this, if we use the second option, then we are calling copy constructor of string, instead of move, because x is rvalue reference that has a name, so it is really lvalue and we need to use move to cast it to be rvalue. Do i miss something, or is it really tutorial's mistake? Btw, adding move doesn't solve my first problem.

Upvotes: 0

Views: 2156

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275320

So anything with a name is an lvalue.

An rvalue reference with a name is an lvalue.

An rvalue reference will bind to rvalues, but it itself is an lvalue.

So x in ptr(x.ptr) is an rvalue reference, but it has a name, so it is an lvalue.

To treat it as an rvalue, you need to do ptr( std::move(x).ptr ).

Of course, this is mostly useless, as moving a ptr does nothing as ptr is a dumb raw pointer.

You should be following the rule of 0 here.

class Example6 {
    std::unique_ptr<string> ptr;
  public:
    Example6 (string str) : ptr(std::make_unique<string>(std::move(str))) {cout << "DONT   MOVE " << '\n';}
    Example6():Example6("") {}
    ~Example6 () = default;
    // move constructor
    Example6 (Example6&& x) = default;
    // move assignment
    Example6& operator= (Example6&& x) = default;
    // access content:
    const string& content() const {
       if (!ptr) *this=Example6{};
       return *ptr;
    }
    // addition:
    Example6 operator+(const Example6& rhs) {
      return Example6(content()+rhs.content());
    }
};

because business logic and lifetime management don't belong intermixed in the same class.

While we are at it:

    // addition:
    Example6& operator+=(const Example6& rhs) & {
      if (!ptr) *this = Example6{};
      *ptr += rhs.content();
      return *this;
    }
    // addition:
    friend Example6 operator+(Example6 lhs, const Example6& rhs) {
      lhs += rhs;
      return lhs;
    }

Upvotes: 3

User8500049
User8500049

Reputation: 422

I can say your class does not have a copy constructor. Because copy ctor parameter have to be const and reference

class Example6{ 
public:
    Example6(const Example6 &r);
};

Upvotes: 0

eerorika
eerorika

Reputation: 238311

Copy constructor is called ... - why?

The premise of your question is faulty: The copy constructor is not called. In fact, the class is not copyable.

The first constructor is a converting constructor from std::string. The converting constructor is called because Example6 objects are initialised with a string argument. Once in each of these expressions:

  • Example6 foo("Exam")
  • Example6("ple")
  • Example6(content() + rhs.content()

... instead of move constructor

There are a few copy-initialisations by move in the program. However, all of them can be elided by the compiler.

Is it somehow possible to prevent it and always call move constructor?

There are a few mistakes that can prevent copy elision. For example, if you wrote the addition operator like this:

return std::move(Example6(content()+rhs.content()));

The compiler would fail to elide the move and probably tell you about it if you're lucky:

warning: moving a temporary object prevents copy elision

Shouldn't it call

ptr(move(x.ptr))

instead of just

ptr(x.ptr)

There's no need. Moving a pointer is exactly the same as copying a pointer. Same holds for all fundamental types.

The way I understand this, if we use the second option, then we are calling copy constructor of string, instead of move

ptr is not a string. It is a pointer to a string. Copying a pointer does nothing to the pointed object.


PS. The example program is quite bad quality. There should never be owning bare pointers in C++.

Upvotes: 1

Related Questions