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.
Pass-by-references is more efficient than pass-by-value, because it does not copy the arguments. The formal parameter is an alias for the argument. When the called function read or write the formal parameter, it is actually read or write the argument itself.
The main difference between pass by value and pass by reference is that, in pass by value, the parameter value copies to another variable while in pass by reference, the actual parameter passes to the function.
If you recall, using pass by reference allows us to effectively “pass” the reference of a variable in the calling function to whatever is in the function being called. The called function gets the ability to modify the value of the argument by passing in its reference.
When you use pass-by-value, the compiler copies the value of an argument in a calling function to a corresponding non-pointer or non-reference parameter in the called function definition. The parameter in the called function is initialized with the value of the passed argument.
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
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)) { }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With