2

Related questions:

I'm posting this question because this thing of the move semantics is really puzzling me. At first they seemed quite clear to me, but when I tried to demostrate the use of those to myself, then I realized that maybe I've misunderstood something.

I've tried to arrange the following file to be a not-so-trivial implementation of a vector-like class making use of move semantics (actually the main function is there as well, together with a free function to make printing to screen easier, ...). It is not really a minimal working example, but the output it produces to screen is reasonably readable, imho.

Still, if you thing it's better to slim it down, please suggest me what to do.

Anyway the code is the following,

#include<iostream>
using namespace std;

int counter = 0; // to keep count of the created objects

class X {
  private:
    int id = 0; // hopefully unique identifyier
    int n = 0;
    int * p;
  public:
    // special member functions (ctor, dtor, ...)
    X()           : id(counter++), n(0),   p(NULL)       { cout << "default ctor (id " << id << ")\n"; }
    X(int n)      : id(counter++), n(n),   p(new int[n]) { cout << "param ctor (id " << id << ")\n"; };
    X(const X& x) : id(counter++), n(x.n), p(new int[n]) {
      cout << "copy ctor (id " << id << ") (allocating and copying " << n << " ints)\n";
      for (int i = 0; i < n; ++i) {
        p[i] = x.p[i];
      }
    };
    X(X&& x)      : id(counter++), n(x.n), p(x.p) {
      cout << "move ctor (id " << id << ")\n";
      x.p = NULL;
      x.n = 0;
    };
    X& operator=(const X& x) {
      cout << "copy assignment (";
      if (n < x.size() && n > 0) {
        cout << "deleting, ";
        delete [] p;
        n = 0;
      }
      if (n == 0) {
        cout << "allocating, and ";
        p = new int[n];
      }
      n = x.size();
      cout << "copying " << n << " values)";
      for (int i = 0; i < n; ++i) {
        p[i] = x.p[i];
      }
      cout << endl;
      return *this;
    };
    X& operator=(X&& x) {
      this->n = x.n;
      this->p = x.p;
      x.p = NULL;
      x.n = 0;
      cout << "move assignment (\"moving\" " << this->n << " values)\n";
      return *this;
    };
    ~X() {
      cout << "dtor on id " << id << " (array of size " << n << ": " << *this << ")\n";
      delete [] p;
      n = 0;
    }
    // getters/setters
    int size() const { return n; }

    // operators
    int& operator[](int i) const { return p[i]; };
    X operator+(const X& x2) const {
      cout << "operator+\n";
      int n = min(x2.size(), this->size());
      X t(n);
      for (int i = 0; i < n; ++i) {
        t.p[i] = this->p[i] + x2.p[i];
      }
      return t;
    };

    // friend function to slim down the cout lines
    friend ostream& operator<<(ostream&, const X&);
};

int main() {
    X x0;
  X x1(5);
  X x2(5);
  x1[2] = 3;
  x2[3] = 4;
  cout << "\nx0 = x1 + x2;\n";
  x0 = x1 + x2;
  cout << "\nX x4(x1 + x2);\n";
  X x4(x1 + x2);
  cout << x4 << endl;
  cout << '\n';
}

// function to slim down the cout lines
ostream& operator<<(ostream& os, const X& x) {
  os << '[';
  for (int i = 0; i < x.size() - 1; ++i) {
    os << x.p[i] << ',';
  }
  if (x.size() > 0) {
    os << x.p[x.size() - 1];
  }
  return os << ']';
}

When I compile and run it with

$ clear && g++ moves.cpp && ./a.out

the output is the following (#-comments are added by hand)

default ctor (id 0)
param ctor (id 1)
param ctor (id 2)

x0 = x1 + x2;
operator+
param ctor (id 3)
move assignment ("moving" 5 values)
dtor on id 3 (array of size 0: [])

X x4(x1 + x2);
operator+
param ctor (id 4)
[0,0,3,4,0]

dtor on id 4 (array of size 5: [0,0,3,4,0])
dtor on id 2 (array of size 5: [0,0,0,4,0])
dtor on id 1 (array of size 5: [0,0,3,0,0])
dtor on id 0 (array of size 5: [0,0,3,4,0])

From the first part of the output, I guess that I really demonstrated the intended use of the move assignment operator. Am I right, in this respect? (From the next output, it seems I'm not, but I'm not sure.)

At this point, if my deduction that copy elision prevented the call to the copy ctor is right, then one question comes natural to me (and not only me, see OP's comment here):

Isn't that situation of creating an object based on another temporary object (e.g. x4 based on the result of x1 + x2 in X x4(x1 + x2);) exactly the one for which move semantics where supposed to be introduced? If not, what is a basic example to show the use of the move ctor?

Then I read that copy elision can be prevented by adding a proper option.

The output of

clear && g++ -fno-elide-constructors moves.cpp && ./a.out 

however, is the following:

default ctor (id 0)
param ctor (id 1)
param ctor (id 2)

x0 = x1 + x2;
operator+
param ctor (id 3)
move ctor (id 4)
dtor on id 3 (array of size 0: [])
move assignment ("moving" 5 values)
dtor on id 4 (array of size 0: [])

X x4(x1 + x2);
operator+
param ctor (id 5)
move ctor (id 6)
dtor on id 5 (array of size 0: [])
move ctor (id 7)
dtor on id 6 (array of size 0: [])
[0,0,3,4,0]

dtor on id 7 (array of size 5: [0,0,3,4,0])
dtor on id 2 (array of size 5: [0,0,0,4,0])
dtor on id 1 (array of size 5: [0,0,3,0,0])
dtor on id 0 (array of size 5: [0,0,3,4,0])
+enrico:CSGuild$ 

where it looks like the call to the move ctor I expect is now there, but both that call and the call to the move assignment are preceded by another call to the move ctor each.

Why is this the case? Have I completely misunderstood the meaning of move semantics?

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • In terms of "slimming it down" ... you have code here for copy and move assignment which is utterly irrelevant to your actual questions (around move/copy **construction**) spurred only by the line `X x4(x1 + x2)` – donkopotamus May 12 '19 at 23:10
  • `X x4; /* ... */; x4 = x1 + x2;` would be a better example of a use case for move semantics – M.M May 12 '19 at 23:31
  • @donkopotamus, I hope my edits to the question clarify that I have doubt on move assignment operator as well. Anyway I could remove some of the lines in the main. I'll do it this evening. – Enlico May 13 '19 at 05:58

1 Answers1

3

You appear to have two questions here:

  • why is the move constructor not called for X x4(x1 + x2)?
  • why, when copy elision is disabled, is the move constructor called twice?

The first question

Isn't that situation (X x4(x1 + x2);) exactly the one for which move semantics where supposed to be introduced?

Well, no. In order to use move semantics you're effectively suggesting that we should choose to construct an X in operator+, and then move it to the result x4, which is clearly inefficient compared to a copy-elided construct the final result (x4) in place during operator+.

The second question

Having disabled copy-elision, why do we see two calls to the move constructor during X x4(x1 + x2)? Consider that there are three scopes in play here:

  1. the operator+ scope, where we construct an X and return it;
  2. the main scope where we are calling X x4(x1 + x2);
  3. the X constructor where we are constructing an X from x1 + x2;

Then, in the absence of elision, the compiler is:

  • moving the result from operator+ to main (into x1 + x2); and
  • moving the content of x1 + x2 into x4.
Enlico
  • 23,259
  • 6
  • 48
  • 102
donkopotamus
  • 22,114
  • 2
  • 48
  • 60
  • The two moves are moving from `t` (local variable of `operator+`) to the result object (a temporary in this case), and moving the result object to `x4`. – M.M May 12 '19 at 23:27
  • @M.M That's what I'm trying to say in those final two bullet points (evidently very unclearly!) – donkopotamus May 12 '19 at 23:33
  • @donkopotamus, how can I edit the code in way that it demonstrates the use of move assignment operator and move ctor? Btw, the last two bullet points are cleart to me. – Enlico May 13 '19 at 05:56
  • @donkopotamus, I think that maybe a good line showing the call to the move ctor is for instance `X x(X(3));`, am I wrong? – Enlico May 13 '19 at 20:26