People do perfect forward constructors.
There are costs.
First, the cost is that they must be in the header file. Second, each use tends to result in a different constructor being created. Third, you cannot use {}
-like initialization syntax for the objects you are constructing from.
Fourth, it interacts poorly with the Foo(Foo const&)
and Foo(Foo&&)
constructors. It will not replace them (due to language rules), but it will be selected over them for Foo(Foo&)
. This can be fixed with a bit of boilerplate SFINAE:
template<class T,
std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0
>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
which now is no longer preferred over Foo(Foo const&)
for arguments of type Foo&
. While we are at it we can do:
Bar bar_;
template<class T,
std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0,
std::enable_if_t<std::is_constructible<Bar, T>{},int> =0
>
Foo(T&& bar) :
bar_{std::forward<T>(bar)}
{};
and now this constructor only works if the argument can be used to construct bar
.
The next thing you'll want to do is to either support {}
style construction of the bar
, or piecewise construction, or varargs construction where you forward into bar.
Here is a varargs variant:
Bar bar_;
template<class T0, class...Ts,
std::enable_if_t<sizeof...(Ts)||!std::is_same<std::decay_t<T0>, Foo>{},int> =0,
std::enable_if_t<std::is_constructible<Bar, T0, Ts...>{},int> =0
>
Foo(T0&&t0, Ts&&...ts) :
bar_{std::forward<T0>(t0), std::forward<Ts>(ts)...}
{};
Foo()=default;
On the other hand, if we add:
Foo(Bar&& bin):bar_(std::move(bin));
we now support Foo( {construct_bar_here} )
syntax, which is nice. However this isn't required if we already have the above varardic (or a similar piecewise construct). Still, sometimes an initializer list is nice to forward, especially if we don't know the type of bar_
when we write the code (generics, say):
template<class T0, class...Ts,
std::enable_if_t<std::is_constructible<Bar, std::initializer_list<T0>, Ts...>{},int> =0
>
Foo(std::initializer_list<T0> t0, Ts&&...ts) :
bar_{t0, std::forward<Ts>(ts)...}
{};
so if Bar
is a std::vector<int>
we can do Foo( {1,2,3} )
and end up with {1,2,3}
within bar_
.
At this point, you gotta wonder "why didn't I just write Foo(Bar)
". Is it really that expensive to move a Bar
?
In generic library-esque code, you'll want to go as far as the above. But very often your objects are both known and cheap to move. So write the really simple, rather correct, Foo(Bar)
and be done with all of the tomfoolery.
There is a case where you have N variables that are not cheap to move and you want efficiency, and you don't want to put the implementation in the header file.
Then you just write a type-erasing Bar
creator that takes anything that can be used to create a Bar
either directly, or via std::make_from_tuple
, and stores the creation for a later date. It then uses RVO to directly construct the Bar
in-place within the target location.
template<class T>
struct make {
using maker_t = T(*)(void*);
template<class Tuple>
static maker_t make_tuple_maker() {
return [](void* vtup)->T{
return make_from_tuple<T>( std::forward<Tuple>(*static_cast<std::remove_reference_t<Tuple>*>(vtup)) );
};
}
template<class U>
static maker_t make_element_maker() {
return [](void* velem)->T{
return T( std::forward<U>(*static_cast<std::remove_reference_t<U>*>(velem)) );
};
}
void* ptr = nullptr;
maker_t maker = nullptr;
template<class U,
std::enable_if_t< std::is_constructible<T, U>{}, int> =0,
std::enable_if_t<!std::is_same<std::decay_t<U>, make>{}, int> =0
>
make( U&& u ):
ptr( (void*)std::addressof(u) ),
maker( make_element_maker<U>() )
{}
template<class Tuple,
std::enable_if_t< !std::is_constructible<T, Tuple>{}, int> =0,
std::enable_if_t< !std::is_same<std::decay_t<Tuple>, make>{}, int> =0,
std::enable_if_t<(0<=std::tuple_size<std::remove_reference_t<Tuple>>{}), int> = 0 // SFINAE test that Tuple is a tuple-like
// TODO: SFINAE test that using Tuple to construct T works
>
make( Tuple&& tup ):
ptr( std::addressof(tup) ),
maker( make_tuple_maker<Tuple>() )
{}
T operator()() const {
return maker(ptr);
}
};
Code uses a C++17 feature, std::make_from_tuple
, which is relatively easy to write in C++11. In C++17 guaranteed elision means it even works with non-movable types, which is really cool.
Live example.
Now you can write:
Foo( make<Bar> bar_in ):bar_( bar_in() ) {}
and the body of Foo::Foo
can be moved out of the header file.
But that is more insane than the above alternatives.
Again, have you considered just writing Foo(Bar)
?