Consider these classes:
#include <iostream>
#include <string>
class A
{
std::string test;
public:
A (std::string t) : test(std::move(t)) {}
A (const A & other) { *this = other; }
A (A && other) { *this = std::move(other); }
A & operator = (const A & other)
{
std::cerr<<"copying A"<<std::endl;
test = other.test;
return *this;
}
A & operator = (A && other)
{
std::cerr<<"move A"<<std::endl;
test = other.test;
return *this;
}
};
class B
{
A a;
public:
B (A && a) : a(std::move(a)) {}
B (A const & a) : a(a) {}
};
When creating a B
, I always have an optimal forward path for A
, one move for rvalues or one copy for lvalues.
Is it possible to achieve the same result with one constructor? It's not a big problem in this case, but what about multiple parameters? I would need combinations of every possible occurrence of lvalues and rvalues in the parameter list.
This is not limited to constructors, but also applies to function parameters (e.g. setters).
Note: This question is strictly about class B
; class A
exists only to visualize how the copy/move calls gets executed.
The "by-value" approach is an option. It is not as optimal as what you have, but only requires one overload:
class B
{
A a;
public:
B (A _a) : a(move(_a)) {}
};
The cost is 1 extra move construction for both lvalues and xvalues, but this is still optimal for prvalues (1 move). An "xvalue" is an lvalue that has been cast to rvalue using std::move.
You could also try a "perfect forwarding" solution:
class B
{
A a;
public:
template <class T,
class = typename std::enable_if
<
std::is_constructible<A, T>::value
>::type>
B (T&& _a) : a(std::forward<T>(_a)) {}
};
This will get you back to the optimal number of copy/move constructions. But you should constrain the template constructor such that it is not overly generic. You might prefer to use is_convertible instead of is_constructible as I've done above. This is also a single constructor solution, but as you add parameters, your constraint gets increasingly complicated.
Note: The reason the constraint is necessary above is because without, clients of B
will get the wrong answer when they query std::is_constructible<B, their_type>::value
. It will mistakenly answer true without a proper constraint on B
.
I would say that none of these solutions is always better than the others. There are engineering tradeoffs to be made here.