据我所知,在C ++ 11中有效实现构造函数的两种常用方法是使用其中两种
Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar) : bar_{std::move(bar)} {};
或只是一个以
的方式Foo(Bar bar) : bar_{std::move(bar)} {};
第一个选项导致最佳性能(例如,在左值的情况下希望是单个副本,在rvalue的情况下是希望单个副本),但是对于N个变量需要2个 N 重载,而传递左值时,第二个选项只需要一个额外的移动费用。
在大多数情况下,这不应该产生太大影响,但肯定两种选择都不是最佳选择。但是,也可以执行以下操作:
template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
这样做的缺点是允许可能不需要的类型的变量作为bar
参数(这是我确信使用模板专门化很容易解决的问题),但无论如何性能是最佳的并且代码增长与变量的数量呈线性关系。
为什么没有人为此目的使用像forward这样的东西?这不是最优化的方式吗?
答案 0 :(得分:21)
人们做完善的前锋建设者。
有成本。
首先,成本是它们必须位于头文件中。其次,每次使用都会导致创建不同的构造函数。第三,你不能使用{}
- 就像你正在构建的对象的初始化语法一样。
第四,它与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
时才有效。
接下来你要做的就是支持{}
的{{1}}样式构造,或分段构造,或者你转向吧的varargs构造。
这是一个varargs变种:
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));
语法,这很好。但是,如果我们已经拥有上述varardic(或类似的分段结构),则不需要这样做。尽管如此,有时候初始化列表很容易转发,特别是如果我们在编写代码时不知道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} )
并{1,2,3}
内的bar_
。
此时,你不得不想知道为什么我不写Foo(Bar)
&#34;。移动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 // 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);
}
};
代码使用C ++ 17特性std::make_from_tuple
,这在C ++ 11中相对容易编写。在C ++ 17中,保证省略意味着它甚至适用于不可移动的类型,这非常酷。
现在你可以写:
Foo( make<Bar> bar_in ):bar_( bar_in() ) {}
并且Foo::Foo
的正文可以移出头文件。
但这比上述替代品更疯狂。
再次,您是否考虑过撰写Foo(Bar)
?