Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is the pass-by-value-and-then-move construct a bad idiom?

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?

like image 935
jbgs Avatar asked Jan 10 '14 02:01

jbgs


People also ask

Does passing by value make a copy?

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.

Is passed by value and only copied once?

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.

What is move constructor C++?

A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying.


2 Answers

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.

like image 137
Casey Avatar answered Nov 05 '22 02:11

Casey


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

  • Small String Optimization and Move Operations and
  • "Want speed? Measure." (by Howard Hinnant)

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.

like image 40
Ali Avatar answered Nov 05 '22 02:11

Ali