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 右值的赋值运算符。 但除此之外,我不明白

幻灯片有错误,应该是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>&& 构造函数更好(因为你得到 {} 而不是 {{}} 构造)。