为什么使用 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&&>
获得完美转发,此声明的证明如下(在误差范围内,我希望答案能启发)
- 当给定右值引用时(或为了完整性 - 没有 this example 中的引用限定),这会以结果为右值的方式折叠引用。使用的规则是
&& &&
-> &&
(上面的规则1
)
- 当给定左值引用时,这会以结果为左值的方式折叠引用。这里使用的规则是
& &&
-> &
(上面的规则2
)
这实际上是让 foo()
将参数转发给上面示例中的 bar()
。这也是您在此处使用 std::forward<Args>
时会得到的行为。
问题 - 为什么要在这些上下文中使用 std::forward
?避免额外的实例化是否证明打破惯例是合理的?
Howard Hinnant 的论文 n2951 指定了 6 个约束条件,std::forward
的任何实现都应在这些约束条件下运行 "correctly"。这些是
- 应将左值作为左值转发
- 应将右值转发为右值
- 不应将右值作为左值转发
- 应该将较少的 cv 限定表达式转发给更多的 cv 限定表达式
- 应该将派生类型的表达式转发到可访问的、明确的基类型
- 不应转发任意类型转换
(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::forward
和 std::move
主要是为了方便。他甚至指出 std::forward
可用于执行 std::forward
和 std::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...
对于那些感兴趣的人,当使用 lvalue
和 rvalue
调用时,比较 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_1
、foo_2
、foo_3
为模板,foo
的实例化会导致3次实例化。然后递归地,如果这些函数导致其他 3 个模板函数的实例化,例如,您可以获得 3*3=9 个实例化。因此,您可以将此实例化链视为一棵树,其中根函数实例化可以导致数千个实例化,产生呈指数增长的涟漪效应。另一方面,像 forward
这样的函数是这个实例化树中的一个叶子。所以避免它的实例化可能只能避免1次实例化。
因此,避免模板实例化爆炸的最佳方法是对 "root" 类 和 "root" 函数的参数类型使用动态多态性,然后仅对时间使用静态多态性在此实例化树中实际上位于上层的关键函数。
因此,在我看来,与使用更具表现力(和更安全)的代码的好处相比,使用 static_cast
代替 forward
来避免实例化是浪费时间。在代码架构级别更有效地管理模板实例化爆炸。
当给出如下结构的代码时
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
的任何实现都应在这些约束条件下运行 "correctly"。这些是
- 应将左值作为左值转发
- 应将右值转发为右值
- 不应将右值作为左值转发
- 应该将较少的 cv 限定表达式转发给更多的 cv 限定表达式
- 应该将派生类型的表达式转发到可访问的、明确的基类型
- 不应转发任意类型转换
(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::forward
和 std::move
主要是为了方便。他甚至指出 std::forward
可用于执行 std::forward
和 std::move
的功能。
"Effective Modern C++" 的部分摘录:
Item 23:Understand std::move and std::forward
...
The story forstd::forward
is similar to that forstd::move
, but whereasstd::move
unconditionally casts its argument to anrvalue
,std::forward
does it only under certain conditions.std::forward
is a conditional cast. It casts to anrvalue
only if its argument was initialized with anrvalue
.
...
Given that bothstd::move
andstd::forward
boil down to casts, the only difference being thatstd::move
always casts, whilestd::forward
only sometimes does, you might ask whether we can dispense withstd::move
and just usestd::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...
对于那些感兴趣的人,当使用 lvalue
和 rvalue
调用时,比较 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_1
、foo_2
、foo_3
为模板,foo
的实例化会导致3次实例化。然后递归地,如果这些函数导致其他 3 个模板函数的实例化,例如,您可以获得 3*3=9 个实例化。因此,您可以将此实例化链视为一棵树,其中根函数实例化可以导致数千个实例化,产生呈指数增长的涟漪效应。另一方面,像 forward
这样的函数是这个实例化树中的一个叶子。所以避免它的实例化可能只能避免1次实例化。
因此,避免模板实例化爆炸的最佳方法是对 "root" 类 和 "root" 函数的参数类型使用动态多态性,然后仅对时间使用静态多态性在此实例化树中实际上位于上层的关键函数。
因此,在我看来,与使用更具表现力(和更安全)的代码的好处相比,使用 static_cast
代替 forward
来避免实例化是浪费时间。在代码架构级别更有效地管理模板实例化爆炸。