1

I am new to C++11 and found move semantics and copy ellision are really great to write elegant and efficient code. However I have some problems would like to ask. Here I write a template class matrix.hpp and use it to test behaviors of move semantics.

#include <vector>
#include <iostream>
using namespace std;

template<class T> class matrix {
public:
    matrix(); // default constructor
    matrix(const matrix<T>& mx); // copy constructor
    matrix(matrix<T>&& mx); // move constructor
    matrix(int rows_, int cols_);

    matrix<T>& operator= (matrix<T>&& mx); // move assignment
    matrix<T>& operator= (const matrix<T>& mx); // copy constructor

   matrix<T> mean(int axis) const;
private:
    int rows, cols;
    std::vector<T> data;
};
template<class T> matrix<T>::matrix(): rows(0), cols(0), data(0) {}
template<class T> matrix<T>::matrix (int rows_, int cols_)
    : rows(rows_), cols(cols_), data(rows * cols) {}
template<class T> matrix<T>::matrix(const matrix<T>& mx) {
    cout << "copy-tor" << endl;
    rows = mx.rows;
    cols = mx.cols;
    data = mx.data;
}
template<class T> matrix<T>::matrix(matrix<T>&& mx) {
    cout << "move-tor" << endl;
    rows = mx.rows;
    cols = mx.cols;
    data = std::move(mx.data);
}
template<class T> matrix<T>& matrix<T>::operator= (const matrix<T>& mx) {
    cout << "copy-assign" << endl;
    if (this != &mx) {
        data.clear();
        cols = mx.cols;
        rows = mx.rows;
        data = mx.data;
    }
    return *this;
}
template<class T> matrix<T>& matrix<T>::operator= (matrix<T>&& mx) {
    cout << "move-assign" << endl;
    if (this != &mx) {
        data.clear();
        rows = mx.rows;
        cols = mx.cols;
        data = std::move(mx.data);
    }
    return *this;
}
template<class T> matrix<T> matrix<T>::mean(int axis) const {
    if (axis == 1) {
      matrix<T> mx(1, cols);
      // HERE compute mean vector ...
      return mx;
    } else if (axis == 0) {
      matrix<T> mx(rows, 1);
      // HERE compute mean vector ...
      return mx;
    }
}

And in the test.cpp I test how copy constructors and move semantics are implemented in the following cases:

#include "matrix.hpp"
matrix<float> f() {
    matrix<float> a(1,2);
    return a;
}
matrix<float> g() {
    matrix<float> *b = new matrix<float>(1,2);
    return *b;
}
int main() {
    matrix<float> a;
    a = f(); // (*)
    cout << "--" << endl;
    a = g(); // (**)
    cout << "--" << endl;
    a = a.mean(1);  // (***)
}

The result is:

move-assign
--
copy-tor
move-assign
--
move-tor
move-assign

The first result is straightforwardly inferred from the definition of move assigment. My guess for the second result is that the compiler will create a temporary of the object *b, and then std::move() this temporary object to a. For the third result it a bit strange. However if I initialize the local object mx in the function's scope mean(int axis) then there is just one move-assign. Could anyone explain it to me? Thanks!

EDIT I just edited the mean(int axis) just as it is in my code.

pateheo
  • 430
  • 1
  • 5
  • 13
  • @M.M thanks! I have updated the question. – pateheo Dec 04 '15 at 22:20
  • @M.M sorry for that. I retype my actual code and try to simplify it. – pateheo Dec 04 '15 at 22:24
  • 1
    `matrix a(1,2);` doesn't compile because there is no constructor that accepts two ints. There are several other compilation errors in your code too. It's important that you post a program that can be compiled exactly as it is written, and produces the output you are asking about. See [this page](http://stackoverflow.com/help/mcve) for assistance with this process. – M.M Dec 04 '15 at 22:25
  • @M.M I hope it can be compiled now... – pateheo Dec 04 '15 at 22:50
  • Looks good now, thanks – M.M Dec 04 '15 at 22:59
  • Interestingly, if `mean` were replaced with `matrix mx(axis?1:rows, axis?cols:1); ... return mx;` then the last `move-tor` disappears. This is because, according to the C++ standard, that is a copy-elision context; but your posted code is not. I don't see any technical reason for the posted code not being a copy-elision context, but it does seem there are a number of situations where copy-elision would be possible but the standard hasn't gotten around to writing them down – M.M Dec 04 '15 at 23:06
  • 1
    @M.M The last case as written already qualifies for copy elision under the standard. It's a matter of whether the compiler's NRVO machinery is sophisticated enough to actually elide the copy. (Clang actually does elide it.) – T.C. Dec 04 '15 at 23:20

1 Answers1

2

Without any optimizations, and something like the following:

T fun() { T b; return b; }

int main() {
  T a;
  a = fun();
}

There are two steps to get the returned b into a. The first is the construction of the right side of the assignment expression

a = /* object move constructed with value returned by fun() */

Then the actual assignment happens. Since the right side of the = is an rvalue, the assignment works by move.

For your first example, the construction of the return is elided, and you only see the output of the assignment.

For your second, since you aren't returning a local, but a pointer to dynamically-allocated memory, there must be a copy made to perform the return. The copy produced will be an rvalue, which is then moved-from in the assignment to a.

For your third example, since mx is a local, it is treated as an rvalue. The construction of the return isn't being elided (for some reason) but since it's a local it will be moved-from in the construction of the return. Since what is returned is then an rvalue, a move assignment follows.

Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
  • Thank for your clarification. For the third example however I just updated the code as it is. And I realized that `mean(int axis) { matrix mx(row,col); /*..*/ return mx}` just returns a single `move-assign`. If the local declaration of `mx` is nested, i.e. `mean(int axis) { if (axis == 0) { matrix mx(row,col); /*..*/ return mx} else if (axis==1) { matrix mx(col, row); /* ... */ return mx;} }`, then the original result occurs. It seems that scope make an extra move constructor. – pateheo Dec 04 '15 at 23:02
  • If you make the situation more complex then [copy elision](http://en.cppreference.com/w/cpp/language/copy_elision) can't be performed – Ryan Haining Dec 04 '15 at 23:04
  • Thanks! Copy elision is really helpful. It somewhat deprecates the use of dynamical allocated pointers, at least in my case. – pateheo Dec 04 '15 at 23:14