使用 std::forward 的构造函数
Constructor using std::forward
据我所知,在 C++11 中有效实现构造函数的两种常见方法是使用其中的两种方法
Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar) : bar_{std::move(bar)} {};
或者只是一个
的时尚
Foo(Bar bar) : bar_{std::move(bar)} {};
第一个选项会产生最佳性能(例如,希望在左值的情况下是单个副本,在右值的情况下是单个移动),但需要 2N 重载对于 N 个变量,而第二个选项只需要一个函数,但在传递左值时需要额外的移动。
在大多数情况下,这应该不会产生太大影响,但肯定这两种选择都不是最佳选择。但是,也可以执行以下操作:
template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
这有一个缺点,即允许将可能不需要的类型的变量作为 bar
参数(我确信使用模板特化很容易解决这个问题),但在任何情况下性能都是最佳的,并且代码随着变量的数量线性增长。
为什么没有人为此目的使用 forward 之类的东西?这不是最优的方式吗?
人们做完美的正向构造函数。
有成本。
首先,代价是它们必须在头文件中。其次,每次使用往往会导致创建不同的构造函数。第三,你不能对你正在构造的对象使用类似 {}
的初始化语法。
第四,它与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(Foo const&)
。我们可以这样做:
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} )
语法,这很好。但是,如果我们已经有了上面的 varardic(或类似的分段构造),则不需要这样做。尽管如此,有时初始化列表还是很适合转发的,尤其是当我们在编写代码(泛型,比如)时不知道 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}
此时,你一定想知道 "why didn't I just write 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 // 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)
?
据我所知,在 C++11 中有效实现构造函数的两种常见方法是使用其中的两种方法
Foo(const Bar& bar) : bar_{bar} {};
Foo(Bar&& bar) : bar_{std::move(bar)} {};
或者只是一个
的时尚Foo(Bar bar) : bar_{std::move(bar)} {};
第一个选项会产生最佳性能(例如,希望在左值的情况下是单个副本,在右值的情况下是单个移动),但需要 2N 重载对于 N 个变量,而第二个选项只需要一个函数,但在传递左值时需要额外的移动。
在大多数情况下,这应该不会产生太大影响,但肯定这两种选择都不是最佳选择。但是,也可以执行以下操作:
template<typename T>
Foo(T&& bar) : bar_{std::forward<T>(bar)} {};
这有一个缺点,即允许将可能不需要的类型的变量作为 bar
参数(我确信使用模板特化很容易解决这个问题),但在任何情况下性能都是最佳的,并且代码随着变量的数量线性增长。
为什么没有人为此目的使用 forward 之类的东西?这不是最优的方式吗?
人们做完美的正向构造函数。
有成本。
首先,代价是它们必须在头文件中。其次,每次使用往往会导致创建不同的构造函数。第三,你不能对你正在构造的对象使用类似 {}
的初始化语法。
第四,它与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(Foo const&)
。我们可以这样做:
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} )
语法,这很好。但是,如果我们已经有了上面的 varardic(或类似的分段构造),则不需要这样做。尽管如此,有时初始化列表还是很适合转发的,尤其是当我们在编写代码(泛型,比如)时不知道 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}
此时,你一定想知道 "why didn't I just write 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 // 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)
?