人们会使用完美转发构造函数。
这样做是有代价的。
首先,它们必须在头文件中。其次,每个使用都可能导致创建不同的构造函数。第三,您不能使用类似{}
的初始化语法来构造对象。
第四,它与Foo(Foo const&)
和Foo(Foo&&)
构造函数相互作用不佳。它不会替换它们(由于语言规则),但它将被选中代替Foo(Foo&)
。这可以通过一些样板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)} {}
现在不再推荐使用
Foo(Foo const&)
作为
Foo&
类型的参数。同时,我们可以执行以下操作:
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)}
{};
现在这个构造函数只有在参数可用于构建bar
时才起作用。
接下来你需要做的是支持{}
样式的bar
构建,或者分步构建,或者可变参数构建,在后者中你将转发到bar
。
这里是一个可变参数的变体:
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;
另一方面,如果我们添加:
Foo(Bar&& bin):bar_(std::move(bin));
我们现在支持
Foo( {construct_bar_here} )
的语法,这很好。然而,如果我们已经有了上述可变参数(或类似的分段构造),则不需要此语法。尽管如此,有时转发初始化列表仍然很有用,特别是当我们编写代码时不知道
bar_
的类型时(例如泛型)。
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)...}
{};
所以如果Bar
是一个std::vector<int>
,我们可以使用Foo( {1,2,3} )
,最终在bar_
中得到{1,2,3}
。
此时,你可能会想“为什么我不直接写Foo(Bar)
呢?”移动一个Bar
真的那么昂贵吗?
在通用库式的代码中,你需要做到上述程度。但很多时候,你的对象既已知又便宜移动。因此,编写非常简单而正确的Foo(Bar)
并完成所有愚蠢的事情。
有一种情况,你有N个不便宜移动的变量,你需要效率,并且你不想将实现放在头文件中。
然后,你只需编写一个类型擦除的Bar
创建器,它接受任何可用于直接或通过std::make_from_tuple
创建Bar
的内容,并将创建存储到稍后的日期。然后,它使用RVO在目标位置中直接就地构造Bar
。
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
>
make( Tuple&& tup ):
ptr( std::addressof(tup) ),
maker( make_tuple_maker<Tuple>() )
{}
T operator()() const {
return maker(ptr);
}
};
代码使用了C++17的一个特性,std::make_from_tuple
,在C++11中编写相对容易。 在C++17中,保证省略意味着它甚至可以与非可移动类型一起使用,这真是太棒了。
实时示例。
现在你可以这样编写:
Foo( make<Bar> bar_in ):bar_( bar_in() ) {}
Foo::Foo
的函数体可以移出头文件。
但是这比上面的方法更疯狂。
再次,你有没有考虑过只写Foo(Bar)
?
typename = std::enable_if_t<std::is_same_v<std::remove_reference_t<T>, Bar>>
来限制类型。 - Tavian Barnesis_constructible<Foo,X>
不对任何类型X
都成立,实际上并不容易编写,特别是对于叶子代码和非模板熟练的用户。 - Kerrek SB