【问题标题】:Herb Sutter's CppCon Perfect Forwarding slidesHerb Sutter 的 CppCon 完美转发幻灯片
【发布时间】:2015-04-12 15:03:25
【问题描述】:

我很难理解 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::stringassignment 运算符无论如何都接受非std::string 右值)。 但除此之外,我不明白

  • 除了右值和其他选项 #4 还可以窃取什么
  • 为什么,如第三张幻灯片上的基准所示,选项 #4 会比 #2 快得多(尤其是在最后一个基准中)。他们不应该做同样的事情吗?

【问题讨论】:

  • std::string 有一个赋值运算符,它采用char const*。如果 LHS 上的 std::string 已经有足够的空间,则此运算符不必分配内存。
  • @dyp 也适用于从std::string 复制。此外,#4 不能接受字符串文字。
  • 由于参数类型的原因,所有其他选项(但完美转发)都需要调用者转换为std::string。如果参数是char const*,则需要分配内存,即使生成的std::string(函数参数)可以在没有额外分配的情况下复制到name_。至于选项 #4,IIRC 幻灯片上有一些错误。一个完美的转发函数将使用std::is_convertible<String, std::string>
  • @LightningRacisinObrit:您熟悉和喜爱的 C++98/03 功能和技术中有 99% 仍可供您使用。如果您愿意,只需使用它们并忽略较新的 C++11 功能。 C++11/14 仍然会给您带来一些好处,而无需您更改 C++98/03 的任何语法。

标签: c++ c++11 perfect-forwarding


【解决方案1】:

幻灯片有错误,应该是std::enable_if<std::is_same<...,实际上演讲中实际展示的幻灯片没有错误,可以看到at 1:16:58

是的,正如@dyp 指出的那样,std::enable_if_t<std::is_convertible<String, std::string>::value>> 更有意义。

【讨论】:

  • 如果仅在函数参数为std::string 时启用该函数,您将无法获得char const* 参数的好处。 (事实上​​,如果没有额外的重载或手动转换,您将无法传递char const*s。)
  • @dyp:可以改用std::is_assignable<std::string, String> 来调整该技术以使其更好一些。作者的一般准则是查看您的实现正在做什么,并使用最能描述您的模板约束实现的特征。
  • 我也是这么想的,但正如 dyp 所说,使用std::is_convertible<String, std::string> 会更好地帮助回答我的问题
  • @HowardHinnant 使用is_assignable 有其自身的问题——std::is_assignable<std::string, double>::valuetrue
  • @T.C.这是什么意义上的问题?也许:您可以将double 分配给string,但这是否令人惊讶?或者:尚不清楚是否应该通过仅仅一个 setter 函数来实现?
【解决方案2】:

“不一样”是您在编写可完美转换的构造函数时使用的模式——当传递的类型是您自己类型的某个变体时,您不想使用此转换器。奇怪的是它被copy-pasta包含在这里。

真的,您想使用“可以将其分配给字符串”的特征:std::enable_if_t<std::is_assignable<std::string, String>::value>>,因为这是您所关心的。你可以更进一步,测试它是否可分配(如果是,使用它),如果它是可转换的(如果是,转换,然后分配),但我不会。

简而言之,条件看起来像是来自相关测试的复制粘贴。你真的不想限制太多。

至于为什么它击败了选项#2,如果你的容器中的std::string 已经分配了内存,它可以从char const* 复制而不分配更多。如果取而代之的是string&&,则char const* 首先转换为string,然后是移动分配的。我们有两个字符串,其中一个被丢弃。

您所看到的是内存分配开销。

完美转发不必分配内存。


现在,为了完整起见,还有另一种选择。实施起来有点疯狂,但它几乎与选项 #4 一样有效,并且几乎没有缺点。

选项 5:键入擦除分配。 assignment_view<std::string>.

编写一个类型擦除“分配给类型T”的类。把它作为你的论据。在里面使用。

这比完美转发更易教。该方法可以是虚拟的,因为我们采用的是具体类型(分配给字符串的具体类型)。类型擦除发生在分配器的构造过程中。为分配自的每种类型生成一些代码,但代码仅限于分配,而不是整个函数体。

每次分配都有一些开销(类似于虚函数调用,主要是由于指令缓存未命中造成的)。所以这并不完美。

您调用a.assign_to(name) 代替name = a 来完成分配以获得最大效率。如果您喜欢语法,可以使用name << std::move(a);

为了获得最大效率,分配擦除视图(无论您想怎么称呼它)只能用于生成一个分配:这允许它优化移动语义。您还可以制作一个智能的,在基于 &&& 的 assign-from 上执行不同的操作,但需要额外的函数指针开销。

here 我键入删除T == ? 的概念。这只需要类型擦除T = ? 的概念。 (我现在可以使用 Ts&&... ctor 对类型擦除对象进行 {} 初始化的语法更好一点:这是我第一次尝试。)

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 返回类型)。一个完美的转发(到T{})ctor 会比block_deduction&lt;U&gt;&amp;&amp; 更好(当你得到{} 而不是{{}} 构造)。

【讨论】:

  • 好吧,我也完全忘记了这个特性,直到 Howard Hinnant 提到它;)——虽然我不完全确定它是否适合这里的特性。 is_convertible 模仿带有 std::string const&amp; 参数的函数的限制。作为 T.C.注意,您可以将double 分配给string,但不能将double 转换为string
  • @dyp 我认为这是string 的一个缺陷。无论如何,添加了该类型擦除实现。您现在可以使用erase_assignment_to&lt;std::string&gt;string_assign_to 作为静态参数。如果您的代码是单行代码,那就太愚蠢了,但对于新用户来说,它可能比手动完美转发更容易使用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-03
  • 2015-04-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多