Since we have move semantics in C++, nowadays it is usual to do
void set_a(A a) { _a = std::move(a); }
The reasoning is that if a
is an rvalue, the copy will be elided and there will be just one move.
But what happens if a
is an lvalue? It seems there will be a copy construction and then a move assignment (assuming A has a proper move assignment operator). Move assignments can be costly if the object has too many member variables.
On the other hand, if we do
void set_a(const A& a) { _a = a; }
There will be just one copy assignment. Can we say this way is preferred over the pass-by-value idiom if we will pass lvalues?
By definition, pass by value means you are making a copy in memory of the actual parameter's value that is passed in, a copy of the contents of the actual parameter.
When the object is passed by value the object will be copied if it has to be copied, i.e., when the object passed is not a temporary. However, if it happens to be a temporary the object may be constructed in place, i.e., any copies may have been elided and you just pay for a move construction.
A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying.
Expensive-to-move types are rare in modern C++ usage. If you are concerned about the cost of the move, write both overloads:
void set_a(const A& a) { _a = a; } void set_a(A&& a) { _a = std::move(a); }
or a perfect-forwarding setter:
template <typename T> void set_a(T&& a) { _a = std::forward<T>(a); }
that will accept lvalues, rvalues, and anything else implicitly convertible to decltype(_a)
without requiring extra copies or moves.
Despite requiring an extra move when setting from an lvalue, the idiom is not bad since (a) the vast majority of types provide constant-time moves and (b) copy-and-swap provides exception safety and near-optimal performance in a single line of code.
But what happens if
a
is an lvalue? It seems there will be a copy construction and then a move assignment (assuming A has a proper move assignment operator). Move assignments can be costly if the object has too many member variables.
Problem well spotted. I wouldn't go as far as to say that the pass-by-value-and-then-move construct is a bad idiom but it definitely has its potential pitfalls.
If your type is expensive to move and / or moving it is essentially just a copy, then the pass-by-value approach is suboptimal. Examples of such types would include types with a fixed size array as a member: It may be relatively expensive to move and a move is just a copy. See also
in this context.
The pass-by-value approach has the advantage that you only need to maintain one function but you pay for this with performance. It depends on your application whether this maintenance advantage outweighs the loss in performance.
The pass by lvalue and rvalue reference approach can lead to maintenance headaches quickly if you have multiple arguments. Consider this:
#include <vector> using namespace std; struct A { vector<int> v; }; struct B { vector<int> v; }; struct C { A a; B b; C(const A& a, const B& b) : a(a), b(b) { } C(const A& a, B&& b) : a(a), b(move(b)) { } C( A&& a, const B& b) : a(move(a)), b(b) { } C( A&& a, B&& b) : a(move(a)), b(move(b)) { } };
If you have multiple arguments, you will have a permutation problem. In this very simple example, it is probably still not that bad to maintain these 4 constructors. However, already in this simple case, I would seriously consider using the pass-by-value approach with a single function
C(A a, B b) : a(move(a)), b(move(b)) { }
instead of the above 4 constructors.
So long story short, neither approach is without drawbacks. Make your decisions based on actual profiling information, instead of optimizing prematurely.
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