11

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.

Community
  • 1
  • 1
Chris Drew
  • 14,926
  • 3
  • 34
  • 54
  • 2
    possible duplicate of [Best way to write constructor of a class who holds a STL container in C++11](http://stackoverflow.com/questions/21963062/best-way-to-write-constructor-of-a-class-who-holds-a-stl-container-in-c11) – Ali Mar 15 '14 at 16:14

3 Answers3

4

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

Community
  • 1
  • 1
odinthenerd
  • 5,422
  • 1
  • 32
  • 61
3

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)) { }
David G
  • 94,763
  • 41
  • 167
  • 253
  • You say copy elision cannot occur here. Is it possible to explain why copy elision is possible in the third case when the rvalue (a 'prvalue') is created using a return value but not in the second case when it is created using std::move (a 'xvalue')? – Chris Drew Mar 15 '14 at 18:38
  • @ChrisDrew Because the compiler knows that the object being returned is no longer needed, so it is within its rights to elide the move. For the second case it can't happen because `DataWrapperByValue(Data data)` is not a move constructor or copy constructor, it is simply a constructor that takes a `Data` object. As such, the copy/move cannot be elided. – David G Mar 15 '14 at 18:49
  • Not using SFINAE to check the for the correct type can cause subtle problems. Imagine if someone adds an explicit constructor from an int to the Data class, you could construct a DataWrapperByValue from an int, or even a float since you don't brace initialize data_ – odinthenerd Mar 16 '14 at 12:09
  • @PorkyBrain But of course this is only an example. I don't want to digress outside the immediate subject matter. – David G Mar 16 '14 at 14:33
  • Fair enough, in the example at hand it is not needed. – odinthenerd Mar 16 '14 at 18:39
0

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.

capturesteve
  • 373
  • 2
  • 10