Herb Sutter 的 CppCon Perfect Forwarding 幻灯片
Herb Sutter's CppCon Perfect Forwarding slides
我无法理解 Herb Sutter 在 CppCon 2014 的演讲 "Back to the Basics! Essentials of Modern C++ Style" (@1:15:00) 中建议的关于完美转发选项的几点。
三张相关幻灯片 (here are the slides online) 如下:
我认为在选项 #4 中,如果 String
的衰减类型与 std::string
相同并且与幻灯片中所述没有区别(否则选项 # 2 和 #4 不等价,并且没有 std::string
接受非 std::string
右值的赋值运算符。
但除此之外,我不明白
- 除了右值和
之外,选项 #4 还可以窃取什么
- 为什么,如第三张幻灯片的基准测试所示,选项 #4 比 #2 快得多(尤其是在最后一张基准测试中)。他们不应该做同样的事情吗?
幻灯片有错误,应该是std::enable_if<std::is_same<...
,其实演讲时实际放的幻灯片没有错误,大家可以看看at 1:16:58:
是的,正如@dyp 指出的那样,std::enable_if_t<std::is_convertible<String, std::string>::value>>
更有意义。
"is not the same as" 是您在编写可完美转换的构造函数时使用的模式——当传递的类型是您自己的类型的某个变体时,您不想使用此转换器。很可能它被 copy-pasta 包含在这里。
真的,您想使用特征 "this can be assigned to a string": std::enable_if_t<std::is_assignable<std::string, String>::value>>
,因为那是您关心的。您可以更进一步,测试它是否可分配(如果是,则使用它),如果它是可转换的(如果是,则转换,然后分配)失败,但我不会。
简而言之,条件看起来像是来自相关测试的copy-pasta。你真的不想限制太多。
至于为什么它击败选项 #2,如果容器中的 std::string
已经分配了内存,它可以从 char const*
复制而不分配更多。相反,如果您采用 string&&
,则 char const*
首先转换为 string
,然后是移动分配。我们有两个字符串,一个被丢弃了。
您看到的是内存分配开销。
完美转发不用分配内存
现在,为了完整起见,还有另一种选择。实施起来有点疯狂,但它几乎与选项 #4 一样有效,而且几乎没有缺点。
选项 5:键入擦除分配。 assignment_view<std::string>
.
写一个class类型擦除"assignment to type T
"。把它作为你的论据。在里面用。
这比完美转发更可教。该方法可以是虚拟的,因为我们采用具体类型(分配给字符串的具体类型)。类型擦除发生在分配器的构造过程中。为每个类型生成一些代码,但代码仅限于赋值,而不是函数的整个主体。
每次赋值都有一些开销(类似于虚函数调用,主要是由于指令缓存未命中导致的开销很大)。所以这并不完美。
您调用 a.assign_to(name)
而不是 name = a
来执行分配以获得最大效率。如果您喜欢这种语法,可以使用 name << std::move(a);
。
为了获得最大效率,赋值擦除视图(随便你怎么称呼它)只能用于产生一个赋值:这允许它优化移动语义。你也可以做一个聪明的人,它在基于 &&
和 &
的分配上做一些不同的事情,但需要一个额外的函数指针开销。
我输入擦除 T == ?
的概念。这只需要类型擦除 T = ?
的概念。 (我现在可以使用类型擦除对象的 Ts&&...
构造函数使 {}
初始化的语法更好一点:这是我的第一次尝试。)
live example 类型擦除到赋值给 std::string
.
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
template<class T>struct tag{using type=T;};
template<class...>struct types{using type=types;};
template<class T>
using block_deduction = typename tag<T>::type;
template<class F, class Sig, class T=void>
struct erase_view_op;
template<class F, class R, class...Ts, class T>
struct erase_view_op<F, R(Ts...), T>
{
using fptr = R(*)(void const*, Ts&&...);
fptr f;
void const* ptr;
private:
template<class U>
erase_view_op(U&& u, int):
f([](void const* p, Ts&&...ts)->R{
U& u = reinterpret_cast<U&>( *static_cast<std::decay_t<U>*>(const_cast<void*>(p)) );
return F{}( u, std::forward<Ts>(ts)... );
}),
ptr( static_cast<void const*>(std::addressof(u)) )
{}
public:
template<class U, class=std::enable_if_t< !std::is_same<std::decay_t<U>,erase_view_op>{} && (std::is_same<void,R>{} || std::is_convertible< std::result_of_t<F(U,Ts...)>, R >{}) >>
erase_view_op(U&& u):erase_view_op( std::forward<U>(u), 0 ){}
template<class U=T, class=std::enable_if_t< !std::is_same<U, void>{} >>
erase_view_op( block_deduction<U>&& u ):erase_view_op( std::move(u), 0 ){}
erase_view_op( erase_view_op const& ) = default;
erase_view_op( erase_view_op&& ) = default;
R operator()( Ts... ts ) const {
return f( ptr, std::forward<Ts>(ts)... );
}
};
struct assign_lhs_to_rhs {
template<class lhs, class rhs>
void operator()(lhs&& l, rhs& r)const {
r = std::forward<lhs>(l);
}
};
template<class T>
using erase_assignment_to = erase_view_op< assign_lhs_to_rhs, void(T&), T >;
using string_assign_to = erase_assignment_to< std::string >;
如前所述,它与类型擦除到 ==
非常相似。我做了一些适度的改进(void
return 类型)。一个完美的转发(到 T{}
)构造函数会比 block_deduction<U>&&
构造函数更好(因为你得到 {}
而不是 {{}}
构造)。
我无法理解 Herb Sutter 在 CppCon 2014 的演讲 "Back to the Basics! Essentials of Modern C++ Style" (@1:15:00) 中建议的关于完美转发选项的几点。
三张相关幻灯片 (here are the slides online) 如下:
我认为在选项 #4 中,如果 String
的衰减类型与 std::string
相同并且与幻灯片中所述没有区别(否则选项 # 2 和 #4 不等价,并且没有 std::string
接受非 std::string
右值的赋值运算符。
但除此之外,我不明白
- 除了右值和 之外,选项 #4 还可以窃取什么
- 为什么,如第三张幻灯片的基准测试所示,选项 #4 比 #2 快得多(尤其是在最后一张基准测试中)。他们不应该做同样的事情吗?
幻灯片有错误,应该是std::enable_if<std::is_same<...
,其实演讲时实际放的幻灯片没有错误,大家可以看看at 1:16:58:
是的,正如@dyp 指出的那样,std::enable_if_t<std::is_convertible<String, std::string>::value>>
更有意义。
"is not the same as" 是您在编写可完美转换的构造函数时使用的模式——当传递的类型是您自己的类型的某个变体时,您不想使用此转换器。很可能它被 copy-pasta 包含在这里。
真的,您想使用特征 "this can be assigned to a string": std::enable_if_t<std::is_assignable<std::string, String>::value>>
,因为那是您关心的。您可以更进一步,测试它是否可分配(如果是,则使用它),如果它是可转换的(如果是,则转换,然后分配)失败,但我不会。
简而言之,条件看起来像是来自相关测试的copy-pasta。你真的不想限制太多。
至于为什么它击败选项 #2,如果容器中的 std::string
已经分配了内存,它可以从 char const*
复制而不分配更多。相反,如果您采用 string&&
,则 char const*
首先转换为 string
,然后是移动分配。我们有两个字符串,一个被丢弃了。
您看到的是内存分配开销。
完美转发不用分配内存
现在,为了完整起见,还有另一种选择。实施起来有点疯狂,但它几乎与选项 #4 一样有效,而且几乎没有缺点。
选项 5:键入擦除分配。 assignment_view<std::string>
.
写一个class类型擦除"assignment to type T
"。把它作为你的论据。在里面用。
这比完美转发更可教。该方法可以是虚拟的,因为我们采用具体类型(分配给字符串的具体类型)。类型擦除发生在分配器的构造过程中。为每个类型生成一些代码,但代码仅限于赋值,而不是函数的整个主体。
每次赋值都有一些开销(类似于虚函数调用,主要是由于指令缓存未命中导致的开销很大)。所以这并不完美。
您调用 a.assign_to(name)
而不是 name = a
来执行分配以获得最大效率。如果您喜欢这种语法,可以使用 name << std::move(a);
。
为了获得最大效率,赋值擦除视图(随便你怎么称呼它)只能用于产生一个赋值:这允许它优化移动语义。你也可以做一个聪明的人,它在基于 &&
和 &
的分配上做一些不同的事情,但需要一个额外的函数指针开销。
T == ?
的概念。这只需要类型擦除 T = ?
的概念。 (我现在可以使用类型擦除对象的 Ts&&...
构造函数使 {}
初始化的语法更好一点:这是我的第一次尝试。)
live example 类型擦除到赋值给 std::string
.
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
template<class T>struct tag{using type=T;};
template<class...>struct types{using type=types;};
template<class T>
using block_deduction = typename tag<T>::type;
template<class F, class Sig, class T=void>
struct erase_view_op;
template<class F, class R, class...Ts, class T>
struct erase_view_op<F, R(Ts...), T>
{
using fptr = R(*)(void const*, Ts&&...);
fptr f;
void const* ptr;
private:
template<class U>
erase_view_op(U&& u, int):
f([](void const* p, Ts&&...ts)->R{
U& u = reinterpret_cast<U&>( *static_cast<std::decay_t<U>*>(const_cast<void*>(p)) );
return F{}( u, std::forward<Ts>(ts)... );
}),
ptr( static_cast<void const*>(std::addressof(u)) )
{}
public:
template<class U, class=std::enable_if_t< !std::is_same<std::decay_t<U>,erase_view_op>{} && (std::is_same<void,R>{} || std::is_convertible< std::result_of_t<F(U,Ts...)>, R >{}) >>
erase_view_op(U&& u):erase_view_op( std::forward<U>(u), 0 ){}
template<class U=T, class=std::enable_if_t< !std::is_same<U, void>{} >>
erase_view_op( block_deduction<U>&& u ):erase_view_op( std::move(u), 0 ){}
erase_view_op( erase_view_op const& ) = default;
erase_view_op( erase_view_op&& ) = default;
R operator()( Ts... ts ) const {
return f( ptr, std::forward<Ts>(ts)... );
}
};
struct assign_lhs_to_rhs {
template<class lhs, class rhs>
void operator()(lhs&& l, rhs& r)const {
r = std::forward<lhs>(l);
}
};
template<class T>
using erase_assignment_to = erase_view_op< assign_lhs_to_rhs, void(T&), T >;
using string_assign_to = erase_assignment_to< std::string >;
如前所述,它与类型擦除到 ==
非常相似。我做了一些适度的改进(void
return 类型)。一个完美的转发(到 T{}
)构造函数会比 block_deduction<U>&&
构造函数更好(因为你得到 {}
而不是 {{}}
构造)。