Pass-by-value resulting in extra move
Asked Answered
C

3

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.

Couplet answered 15/3, 2014 at 15:23 Comment(1)
possible duplicate of Best way to write constructor of a class who holds a STL container in C++11Kerakerala
F
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)) { }
Filipino answered 15/3, 2014 at 16:55 Comment(5)
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')?Couplet
@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.Filipino
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_Theotokos
@PorkyBrain But of course this is only an example. I don't want to digress outside the immediate subject matter.Filipino
Fair enough, in the example at hand it is not needed.Theotokos
T
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

Theotokos answered 15/3, 2014 at 16:50 Comment(0)
F
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)) { }
Filipino answered 15/3, 2014 at 16:55 Comment(5)
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')?Couplet
@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.Filipino
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_Theotokos
@PorkyBrain But of course this is only an example. I don't want to digress outside the immediate subject matter.Filipino
Fair enough, in the example at hand it is not needed.Theotokos
D
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.

Dauphine answered 15/3, 2014 at 16:11 Comment(3)
To be more explicit: The first move is from d2 to data and the second from data to data_.Foremast
Your example is not accurate, also DataWrapperByValue does not take an lvalue. It moves or copies its argument depending on its value-category.Filipino
@ChrisDrew, The move constructor of Data is called instead of the copy constructor, so it can be correct.Dauphine

© 2022 - 2024 — McMap. All rights reserved.