为什么要使用std :: forward <t>而不是static_cast <t &&>

时间:2018-11-12 07:50:31

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

在给出以下结构的代码时

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

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

给出该语言的参考折叠和模板推导规则。通过static_cast<Args&&>,我们可以实现完美的转发,该声明的证据在下面(误差范围内,我希望能有所启发)

  • 给定右值引用时(或出于完整性考虑,没有this example中的引用限定条件),这会将引用折叠起来,结果是右值。使用的规则是&& &&-> &&(上面的规则1
  • 给定的左值引用时,这会折叠引用,使结果为左值。此处使用的规则是& &&-> &(上面的规则2

在上面的示例中,这实际上是让foo()将参数转发到bar()。这也是您在此处使用std::forward<Args>时得到的行为。


问题-为什么在这些情况下完全使用std::forward?避免额外的实例化是否符合打破常规的要求?

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

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

(1)和(2)在上述static_cast<Args&&>下可以正常工作。 (3)-(6)在这里不适用,因为在推论上下文中调用函数时,这些都不会发生。


注意:我个人更喜欢使用std::forward,但是我的辩解纯粹是我更愿意遵循惯例。

3 个答案:

答案 0 :(得分:13)

Scott Meyers说std::forwardstd::move主要是为了方便。他甚至指出std::forward可用于执行std::forwardstd::move的功能。
摘自“有效的现代C ++”:

  

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

对于那些感兴趣的人,在用std::forward<T>static_cast<T&&>进行调用时,比较assemblylvaluervalue的情况(无任何优化)。

答案 1 :(得分:7)

9.25+ enterd by user 23.7= enterd by user 32.95 this is your result 表达了这种意图,并且使用起来可能比forward更安全:static_cast考虑了转换,但是使用static_cast会检测到一些危险的且可能是非故意的转换:

forward

错误消息的示例:compiler explorer link


此证明的适用方式:通常,证明这一点的原因是使用static_cast避免了不必要的模板实例化。

编译时间是否比代码可维护性更成问题?编码器是否应该考虑将代码中每一行的“不必要的模板实例化”减到最少,甚至会浪费时间?

实例化模板时,其实例化会导致在模板的定义和声明中使用模板的实例化。这样,例如,如果您具有以下功能:

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
  }

其中 template<class T> void foo(T i){ foo_1(i),foo_2(i),foo_3(i); } foo_1foo_2是模板,foo_3的实例化将导致3个实例化。然后,如果这些函数导致其他3个模板函数的实例化,则可以递归地获得3 * 3 = 9实例化。因此,您可以将此实例化链视为一棵树,其中根函数实例化可以导致成千上万个实例化,并以指数级增长的涟漪效应产生。另一方面,类似foo的函数是此实例化树中的叶子。因此,避免实例化可能只会避免1个实例化。

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

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

答案 2 :(得分:5)

这里是我的2.5分,而不是技术上的分:今天std::forward确实只是一个普通的static_cast<T&&>事实,并不意味着明天也将以完全相同的方式实施。我认为委员会需要一些东西来反映std::forward在今天取得的成就的理想行为,因此forward并没有在任何地方转发任何东西。

从理论上讲,在std::forward的保护下正式形成了所需的行为和期望后,没有人阻止将来的实施者提供std::forward而不是static_cast<T&&>而是他的特定内容自己的实现,而没有实际考虑static_cast<T&&>,因为重要的唯一事实是std::forward的正确用法和行为。