0

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.

user4581301
  • 33,082
  • 7
  • 33
  • 54
Bluerain
  • 9
  • 4
  • 1
    `ptr` is a pointer, not a `string`. Either way will just copy the pointer and neither will call a constructor. – François Andrieux Nov 07 '18 at 20:15
  • 1
    You are not printing the copy constructor, you are printing the main constructor. Most likely a dupe of: https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization – NathanOliver Nov 07 '18 at 20:17
  • "... found some info about elision. Is it somehow possible to prevent it" - Why would you want that? Eliding the copy or move is always going to be faster than doing either of the former, and if your copy/move constructors/assignment operators are correct then the end result will be the same. Elision is a *good* thing. – Jesper Juhl Nov 07 '18 at 20:18
  • 2
    First constructor is not a copy constructor. Your class does not have a copy constructor at all. – user7860670 Nov 07 '18 at 20:27
  • 1
    All that manual memory management is really not needed in modern C++ an you should not do it. – Jesper Juhl Nov 07 '18 at 20:58

3 Answers3

3

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;
    }
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • As far as I can tell, rule of 3/5/0 is *not* violated in the example. Destructor, move assignment and move constructor are implemented - rule of 3 (for non-copyable classes). – eerorika Nov 07 '18 at 20:49
1

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

Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • "_There should never be owning bare pointers in C++._" Are you saying that you shouldn't ever write a smart pointer class? It seems slightly over the top. – curiousguy Nov 08 '18 at 00:21
  • 1
    @curiousguy You're correct. I've simplified my advice for the audience of this answer/question - beginners. Given the smart pointers offered by the standard library, there is quite rarely need to implement more, and when there is, you might be able to build them upon those existing structures. – eerorika Nov 08 '18 at 00:34
0

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);
};
User8500049
  • 410
  • 3
  • 12
  • "_Because copy ctor parameter have to be const_" No, technically it doesn't have to. But the **normal** copy constructor semantic implies that the value or the state of the source isn't changed and good const-safety practice means that such parameters that shouldn't be modified by a function *should be* made const. So the copy constructor *should usually have* a const parameter. But it isn't a "shall" by language definition, and `Example6(Example6&)` is a **legal** copy constructor, but one that raises questions. Why non const? Why change the source? – curiousguy Nov 08 '18 at 00:33
  • As an example of such class with non-const copy constructor is the deprecated `std::auto_ptr` (which is not a great design). – eerorika Nov 08 '18 at 00:47