【问题标题】:Why use std::forward<T> instead of static_cast<T&&>为什么使用 std::forward<T> 而不是 static_cast<T&&>
【发布时间】:2019-04-14 22:01:50
【问题描述】:

当给出以下结构的代码时

template <typename... Args>
void foo(Args&&... args) { ... }

我经常看到库代码在函数中使用static_cast&lt;Args&amp;&amp;&gt; 进行参数转发。通常,这样做的理由是使用static_cast 可以避免不必要的模板实例化。

给定语言的引用折叠和模板推导规则。我们通过static_cast&lt;Args&amp;&amp;&gt; 获得了完美的转发,此声明的证据如下(在误差范围内,我希望答案能启发)

  • 当给定右值引用时(或为了完整性 - 没有像this example 中的引用限定),这会以结果为右值的方式折叠引用。使用的规则是&amp;&amp; &amp;&amp; -> &amp;&amp;(上面的规则1
  • 当给定左值引用时,这会折叠引用以使结果是左值。这里使用的规则是&amp; &amp;&amp; -> &amp;(上面的规则2

这实质上是让foo() 将参数转发到上面示例中的bar()。这也是您在此处使用std::forward&lt;Args&gt; 时会得到的行为。


问题 - 为什么在这些情况下使用 std::forward?避免额外的实例化是否有理由打破约定?

Howard Hinnant 的论文 n2951 指定了 6 个约束条件,std::forward 的任何实现都应该“正确”地运行。这些是

  1. 应该将左值作为左值转发
  2. 应该将右值作为右值转发
  3. 不应将右值作为左值转发
  4. 应将较少的 cv 限定表达式转发到更多 cv 限定的表达式
  5. 应将派生类型的表达式转发到可访问、明确的基类型
  6. 不应转发任意类型转换

(1) 和 (2) 被证明可以与上面的 static_cast&lt;Args&amp;&amp;&gt; 一起正常工作。 (3) - (6) 在这里不适用,因为在推导的上下文中调用函数时,这些都不会发生。


注意:我个人更喜欢使用std::forward,但我的理由纯粹是我更喜欢遵守约定。

【问题讨论】:

  • 我认为在某些模板库中,人们避免使用std::forward,因为他们希望库是独立的,并且不想使用标准库。他们还将实现自己的std::move等。
  • @VTT 在这种情况下不是,因为左值通常被推导出为T&amp; - wandbox.org/permlink/hPucHiFB2pwh53Ox
  • std::forwardstd::move 是为了便于阅读,您可以使用static_cast 来获得相同的行为
  • MM 说了什么。在这个answer 中,霍华德提到std::move 的引入是为了使提案更受欢迎。然后就卡住了。我想std::forward也是如此。
  • @M.M 您关于可读性的观点适用。然而,std::move 在我看来略有不同,因为我没有看到它的实现会以它的方式来防止用户错误,使它成为一个更简单的 1-1 映射映射。 std::forward 通常有两个重载,大概是为了安全,这一事实让我认为这种情况可能更复杂。虽然,我没有充分的理由证明这一点。因此,我猜这个问题。

标签: c++ templates c++17 rvalue-reference forwarding


【解决方案1】:

Scott Meyers 说std::forwardstd::move 主要是为了方便。他甚至表示std::forward 可用于执行std::forwardstd::move 的功能。
《Effective Modern C++》的部分节选:

第 23 项:了解 std::move 和 std::forward
...
std::forward 的故事与 std::move 的故事相似,但 std::move 无条件地将其参数转换为 rvaluestd::forward 仅在某些条件下才会这样做。 std::forward 是一个条件转换。仅当其参数使用 rvalue 初始化时,它才会强制转换为 rvalue
...
鉴于std::movestd::forward 都归结为强制转换,唯一的区别是std::move 总是强制转换,而std::forward 只是有时会,你可能会问我们是否可以省去std::move 而只是在任何地方使用std::forward。从纯粹的技术角度来看,答案是肯定的:std::forward 可以做到这一切。 std::move 不是必需的。 当然,这两个函数都不是必需的,因为我们可以在任何地方编写强制转换,但我希望我们同意那会很糟糕。
...
std::move 的吸引力在于方便、减少出错的可能性和更清晰...

对于那些感兴趣的人,比较assembly 中的std::forward&lt;T&gt;static_cast&lt;T&amp;&amp;&gt;(未进行任何优化)在使用lvaluervalue 调用时。

【讨论】:

  • 如果我想将左值更改为右值,我必须使用std::move,而不是std::forward。对吗?
  • 是的。 move 将其参数转换为右值。
  • 有点期待你的评论。 :-) 我不认为斯科特迈耶斯意味着我们可以用前进代替前进。他在那个项目中举了一个例子,在前进方面实施移动。 " std::move 只需要一个函数参数 (rhs.s),而 std::forward 需要一个函数参数 (rhs.s) 和一个模板类型参数 (std::string)。然后注意我们传递的类型to std::forward 应该是一个非引用,因为这是编码的约定,传递的参数是一个右值(参见第 28 条)。继续...
  • ...总之,这意味着 std::move 比 std::forward 需要更少的输入,并且它省去了我们传递类型参数的麻烦,该参数编码我们传递的参数是一个右值。它还消除了我们传递不正确类型的可能性(例如,std::string&,这将导致数据成员 s 被复制构造而不是移动构造)。更重要的是,使用 std::move 将无条件强制转换为右值,而使用 std::forward 表示仅对已绑定右值的引用强制转换为右值。
  • @M.M:是的,看起来 Scott Meyers 没有考虑到这一点。我是逐字引用他的话。 :)
【解决方案2】:

forward 表达了意图,使用起来可能比static_cast 更安全:static_cast 考虑转换,但使用forward 检测到一些危险且可能是非故意的转换:

struct A{
   A(int);
   };

template<class Arg1,class Arg2>
Arg1&& f(Arg1&& a1,Arg2&& a2){
   return static_cast<Arg1&&>(a2); //  typing error: a1=>a2
   }

template<class Arg1,class Arg2>
Arg1&& g(Arg1&& a1,Arg2&& a2){
   return forward<Arg1>(a2); //  typing error: a1=>a2
   }

void test(const A a,int i){
   const A& x = f(a,i);//dangling reference
   const A& y = g(a,i);//compilation error
  }

错误信息示例:compiler explorer link


如何应用这个理由:通常,这样做的理由是使用 static_cast 避免了不必要的模板实例化。

编译时间比代码可维护性更成问题吗?编码员是否应该浪费时间考虑在代码中的每一行最小化“不必要的模板实例化”?

当一个模板被实例化时,它的实例化会导致模板的实例化,在它的定义和声明中使用。因此,例如,如果您有以下功能:

  template<class T> void foo(T i){
     foo_1(i),foo_2(i),foo_3(i);
     }

其中foo_1,foo_2,foo_3是模板,foo的实例化会导致3个实例化。然后递归地如果这些函数导致其他 3 个模板函数的实例化,例如,您可以获得 3*3=9 实例化。因此,您可以将这个实例化链视为一棵树,其中根函数实例化可以导致数千个实例化为指数增长的连锁反应。另一方面,像forward 这样的函数是这个实例化树中的一个叶子。所以避免它的实例化可能只能避免1个实例化。

因此,避免模板实例化爆炸的最佳方法是对“根”类和“根”函数的参数类型使用动态多态性,然后仅对在此实例化树中实际上处于较高位置的时间关键函数使用静态多态性.

因此,在我看来,使用static_cast 代替forward 来避免实例化与使用更具表现力(和更安全)代码的好处相比是浪费时间。在代码架构级别更有效地管理模板实例化爆炸。

【讨论】:

  • 检查的不是conversion,检查只是针对右值->左值转发。如果将g(a, i) 更改为g(std::move(a), i),则上面的代码将编译。
  • 这个大括号样式怎么叫?
  • @liliscent 不一定,您可以像您说的那样进行这项工作,但在这种情况下会阻止转换。我认为这是std::forward 实施的结果。它并不能阻止所有可能的脚枪,但至少可以阻止其中一些。这是static_caststd::forward 之间的唯一区别。因此,对我来说,使用std::forward 而不是static_cast 是一个激励的理由。
  • 我花了一段时间才弄清楚forwardstatic_cast 之间的区别的例子:在这个例子中,用户试图转发第二个参数,就好像它是第一个参数一样。
【解决方案3】:

这是我的 2.5,不是那么技术性的美分:今天 std::forward 确实只是一个普通的旧 static_cast&lt;T&amp;&amp;&gt; 并不意味着明天它也会以完全相同的方式实现。我认为委员会需要一些东西来反映std::forward 今天所实现的期望行为,因此,不会在任何地方转发任何东西的forward 应运而生。

std::forward 的保护伞下正式确定所需的行为和期望,从理论上讲,没有人会阻止未来的实施者提供std::forward,而不是static_cast&lt;T&amp;&amp;&gt;,而是他自己的特定实施,而实际上没有采取考虑到static_cast&lt;T&amp;&amp;&gt;,因为唯一重要的是std::forward 的正确用法和行为。

【讨论】:

  • 我真诚地怀疑forward 会做其他事情。想想会破坏多少代码......
  • @Rakete1111 这就是为什么我的回答只是假设性的、值得商榷的和纯虚拟的:)
猜你喜欢
  • 2016-11-10
  • 1970-01-01
  • 1970-01-01
  • 2014-06-12
  • 1970-01-01
  • 2015-07-23
  • 2020-02-10
  • 2016-10-24
  • 2010-10-14
相关资源
最近更新 更多