Question

I'm trying to understand move semantics and copy/move elision.

I would like a class that wraps up some data. I would like to pass the data in in the constructor and I would like to own the data.

After reading this, this and this I got the impression that in C++11 if I want to store a copy then pass-by-value should be at least as efficient as any other option (apart from the minor issue of increased code size).

Then if the calling code would like to avoid a copy, it can by passing an rvalue instead of an lvalue. (e.g using std::move)

So I tried it out:

#include <iostream>

struct Data {
  Data()                 { std::cout << "  constructor\n";}
  Data(const Data& data) { std::cout << "  copy constructor\n";} 
  Data(Data&& data)      { std::cout << "  move constructor\n";}
};

struct DataWrapperWithMove {
  Data data_;
  DataWrapperWithMove(Data&& data) : data_(std::move(data)) { }
};

struct DataWrapperByValue {
  Data data_;
  DataWrapperByValue(Data data) : data_(std::move(data)) { }
};

Data
function_returning_data() {
  Data d;
  return d;
}

int main() {
  std::cout << "1. DataWrapperWithMove:\n"; 
  Data d1;
  DataWrapperWithMove a1(std::move(d1));

  std::cout << "2. DataWrapperByValue:\n";  
  Data d2;
  DataWrapperByValue a2(std::move(d2));

  std::cout << "3. RVO:\n";
  DataWrapperByValue a3(function_returning_data());
}

Output:

1. DataWrapperWithMove:
  constructor
  move constructor
2. DataWrapperByValue:
  constructor
  move constructor
  move constructor
3. RVO:
  constructor
  move constructor

I was pleased that in none of these cases is a copy constructor called but why is there an extra move constructor called in the second case? I guess any decent move constructor for Data should be pretty quick but it still niggles me. I am tempted to use pass-by-rvalue-reference (the first option) instead as this seems to result in one less move constructor call but I would like to embrace pass-by-value and copy elision if I can.

Was it helpful?

Solution 2

DataWrapperByValue has this constructor:

DataWrapperByValue(Data data);

It takes its argument by value which means that depending on whether it is an lvalue or rvalue, it will call the data parameter's copy or move-constructor. In particular: if it is an lvalue, it's copied. If it is an rvalue, it's moved.

Since you are passing in an rvalue via std::move(d2), the move constructor is called to move d2 into the parameter. The second move constructor call is of course via the initilization of the data_ data member.

Unfortunately, copy-elision cannot occurr here. If moves are expensive and you would like to limit them, you can allow perfect forwarding so there is at least one move or one copy:

template<class U>
DataWrapperByValue(U&& u) : data_(std::forward<U>(u)) { }

OTHER TIPS

DataWrapperByValue::data_ is moved from DataWrapperByValue::DataWrapperByValue(Data data)s arguement data which is moved in from d2.

Your conclusion to pass-by-rvalue-reference together with a by value version for cases where you get an l-value yields the best performance. However this is widely considered premature optimization. Howard Hinnant (Best way to write constructor of a class who holds a STL container in C++11) and Sean Parent (http://channel9.msdn.com/Events/GoingNative/2013/Inheritance-Is-The-Base-Class-of-Evil) have both noted that they consider this premature optimization. The reason is that moves are supposed to be verry cheap and to avoid them in this case would cause code duplication, especially if you have more than one arguement that can either be an r or l-value. If by profileing or testing you find that this actuall does degrade performance you can always easily add the pass-by-rvalue-reference after the fact.

A useful pattern in a case where you do need the extra performance is:

struct DataWrapperByMoveOrCopy {
  Data data_;
  template<typename T, 
    typename = typename std::enable_if<    //SFINAE check to make sure of correct type
        std::is_same<typename std::decay<T>::type, Data>::value
    >::type
  >
  DataWrapperByMoveOrCopy(T&& data) : data_{ std::forward<T>(data) } { }
};

here the constructor always does the right thing as can be seen in my live example: http://ideone.com/UsltRA

The advantage of this argueably complex code is probably not relevant with a single arguement but imagine if your constructor had 4 arguements which could be r or l-values, this is much better than writing 16 different constructors.

struct CompositeWrapperByMoveOrCopy {
  Data data_;
  Foo foo_;
  Bar bar_;
  Baz baz_;
  template<typename T, typename U, typename V, typename W, 
    typename = typename std::enable_if<
        std::is_same<typename std::decay<T>::type, Data>::value &&
        std::is_same<typename std::decay<U>::type, Foo>::value &&
        std::is_same<typename std::decay<V>::type, Bar>::value &&
        std::is_same<typename std::decay<W>::type, Baz>::value
    >::type
  >
  CompositeWrapperByMoveOrCopy(T&& data, U&& foo, V&& bar, W&& baz) : 
  data_{ std::forward<T>(data) },
  foo_{ std::forward<U>(foo) },
  bar_{ std::forward<V>(bar) },
  baz_{ std::forward<W>(baz) } { }
};

Note that you can omit the SFINAE check but this allows subtle problems like implicitly converting using explicit constructors. Also without checking the argument types conversions are deferred to inside the consttructor where there are different access rights, different ADL etc. see live example: http://ideone.com/yb4e3Z

I believe it is because you are essentially doing this code.

std::cout << "2. DataWrapperByValue:\n";  
Data d2;
DataWrapperByValue a2(Data(std::move(d2))); // Notice a Data object is constructed. 

Notice DataWrapperByValue only has a constructor that accepts an lvalue. When you do std::move(d2), you are passing an r-value, so another Data object will be created to pass to the DataWrapperByValue constructor. This one is created using the Data(Data&&) constructor. Then the second move constructor is called during DataWrapperByValue's constructor.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top