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

Why use std::forward<T> instead of static_cast<T&&>

当给出如下结构的代码时

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

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

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

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


问题 - 为什么要在这些上下文中使用 std::forward?避免额外的实例化是否证明打破惯例是合理的?

Howard Hinnant 的论文 n2951 指定了 6 个约束条件,std::forward 的任何实现都应在这些约束条件下运行 "correctly"。这些是

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

(1) 和 (2) 被证明可以与上面的 static_cast<Args&&> 一起正常工作。 (3) - (6) 不适用于此处,因为在推导的上下文中调用函数时,可能会发生 none。


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

这是我的 2.5,这不是那么技术性的:今天 std::forward 确实只是一个普通的旧 static_cast<T&&> 并不意味着明天它也将完全按照同样的方式。我认为委员会需要一些东西来反映 std::forward 今天取得的预期行为,因此,不转发任何内容的 forward 应运而生。

std::forward 的保护伞下将所需的行为和期望正式化,从理论上讲,没有人会阻止未来的实施者提供 std::forward 而不是 static_cast<T&&> 的东西具体他自己的实现,没有实际考虑 static_cast<T&&> 因为唯一重要的事实是 std::forward.

的正确用法和行为

Scott Meyers 说 std::forwardstd::move 主要是为了方便。他甚至指出 std::forward 可用于执行 std::forwardstd::move 的功能。
"Effective Modern C++" 的部分摘录:

Item 23:Understand std::move and std::forward
...
The story for std::forward is similar to that for std::move, but whereas std::move unconditionally casts its argument to an rvalue, std::forward does it only under certain conditions. std::forward is a conditional cast. It casts to an rvalue only if its argument was initialized with an rvalue.
...
Given that both std::move and std::forward boil down to casts, the only difference being that std::move always casts, while std::forward only sometimes does, you might ask whether we can dispense with std::move and just use std::forward everywhere. From a purely technical perspective, the answer is yes: std::forward can do it all. std::move isn’t necessary. Of course, neither function is really necessary, because we could write casts everywhere, but I hope we agree that that would be, well, yucky.
...
std::move’s attractions are convenience, reduced likelihood of error, and greater clarity...

对于那些感兴趣的人,当使用 lvaluervalue 调用时,比较 assembly 中的 std::forward<T>static_cast<T&&>(没有任何优化)。

forward 表达了意图,使用它可能比 static_cast 更安全:static_cast 考虑转换,但使用 [=12= 检测到一些危险的和据称无意的转换]:

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 避免了不必要的模板实例化。

编译时间比代码可维护性更成问题吗?编码人员是否应该浪费时间考虑在代码的每一行最小化 "unnecessary template instantiation"?

实例化模板时,其实例化会导致在其定义和声明中使用的模板实例化。因此,例如,如果您有一个函数:

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

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

因此,避免模板实例化爆炸的最佳方法是对 "root" 类 和 "root" 函数的参数类型使用动态多态性,然后仅对时间使用静态多态性在此实例化树中实际上位于上层的关键函数。

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