简单来说...
TL;DR,您希望保留函子的值类别(r-value/l-value 性质),因为这会影响重载决议,特别是ref-qualified members。
函数定义缩减
为了专注于转发函数的问题,我将示例(并使用 C++11 编译器编译)简化为;
template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
return std::forward<F>(func)(std::forward<Args>(args)...);
}
我们创建了第二种形式,我们std::forward(func)
用 just替换了func
;
template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
return func(std::forward<Args>(args)...);
}
样品评估
评估其行为方式的一些经验证据(使用符合标准的编译器)是评估代码示例为何如此编写的一个很好的起点。因此,另外我们将定义一个通用函子;
struct Functor1 {
int operator()(int id) const
{
std::cout << "Functor1 ... " << id << std::endl;
return id;
}
};
初始样本
运行一些示例代码;
int main()
{
Functor1 func1;
apply_impl_2(func1, 1);
apply_impl_2(Functor1(), 2);
apply_impl(func1, 3);
apply_impl(Functor1(), 4);
}
并且输出与预期的一样,与调用和调用重载调用运算符时是使用 r 值还是使用Functor1()
l 值无关。r 值和 l 值都调用它。在 C++03 下,这就是你所得到的,你不能重载基于对象的“r-value-ness”或“l-value-ness”的成员方法。func
apply_impl
apply_impl_2
Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4
Ref-qualified 样品
我们现在需要重载该调用运算符以进一步扩展它......
struct Functor2 {
int operator()(int id) const &
{
std::cout << "Functor2 &... " << id << std::endl;
return id;
}
int operator()(int id) &&
{
std::cout << "Functor2 &&... " << id << std::endl;
return id;
}
};
我们运行另一个样本集;
int main()
{
Functor2 func2;
apply_impl_2(func2, 5);
apply_impl_2(Functor2(), 6);
apply_impl(func2, 7);
apply_impl(Functor2(), 8);
}
输出是;
Functor2 &... 5
Functor2 &... 6
Functor2 &... 7
Functor2 &&... 8
讨论
在apply_impl_2
( id
5 和 6) 的情况下,输出可能不像最初预期的那样。在这两种情况下,都会调用限定operator()
的左值(根本不调用右值)。可能已经预料到,因为Functor2()
,一个 r 值,用于调用apply_impl_2
合格的 r 值operator()
将被调用。,func
作为 的命名参数apply_impl_2
,是一个右值引用,但由于它被命名,它本身就是一个左值。因此,在左值作为参数和右值用作参数operator()(int) const&
的情况下,都会调用限定的左值。func2
Functor2()
在apply_impl
( id
7 和 8)的情况下,std::forward<F>(func)
保持或保留为 提供的论点的 r 值/l 值性质func
。因此,operator()(int) const&
使用左值func2
作为参数调用限定operator()(int)&&
的左值,当使用右值Functor2()
作为参数时,调用限定的右值。这种行为是预期的。
结论
的使用std::forward
,通过完美转发,确保我们保留原始参数的 r-value/l-value 性质func
。它保留了它们的价值类别。
它不仅需要、std::forward
可以而且应该用于将参数转发给函数,而且还需要在必须保留 r-value/l-value 性质的情况下使用参数。笔记; 在某些情况下,不能或不应该保留 r 值/l 值,在这些情况下std::forward
不应使用(参见下面的反面)。
出现了许多示例,它们通过看似无辜地使用 r 值引用而无意中失去了参数的 r 值/l 值性质。
编写定义良好且合理的通用代码一直很困难。随着 r 值引用的引入,尤其是引用折叠,可以更简洁地编写更好的通用代码,但我们需要更加了解所提供参数的原始性质是什么,并确保当我们在我们编写的通用代码中使用它们时,它们会被维护。
完整的示例代码可以在这里找到
推论和逆向
- 这个问题的推论是;给定模板函数中的引用折叠,如何保持参数的右值/左值性质?答案 - 使用
std::forward<T>(t)
。
- 交谈;
std::forward
能解决你所有的“通用参考”问题吗?不,它没有,在某些情况下不应该使用它,例如多次转发值。
完美转发的简要背景
完美转发可能有些人不熟悉,那么完美转发是什么?
简而言之,完美转发是为了确保提供给函数的参数被转发(传递)到另一个具有与最初提供的值类别(基本上是 r 值与 l 值)相同的函数。它通常与可能发生引用折叠的模板函数一起使用。
Scott Meyers 在他的Going Native 2013 演示文稿中给出了以下伪代码来解释std::forward
(大约 20 分钟)的工作原理;
template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
if (is_lvalue_reference<T>::value) {
return param; // return type T&& collapses to T& in this case
}
else {
return move(param);
}
}
完美的转发依赖于少数 C++11 新的基本语言结构,它们构成了我们现在在泛型编程中看到的大部分内容的基础:
目前的使用std::forward
是在公式化的std::forward<T>
,理解如何std::forward
工作有助于理解为什么会这样,并且还有助于识别右值的非惯用或不正确的使用、引用折叠等。
Thomas Becker 提供了一篇关于完美转发问题和解决方案的精彩但密集的文章。
什么是 ref 限定符?
ref-qualifiers(lvalue ref-qualifier&
和 rvalue ref-qualifier &&
)与 cv-qualifiers 类似,因为它们(ref-qualified members)在重载决议期间用于确定调用哪个方法。他们的行为与您期望的一样;&
适用于左值和右&&
值。注意:与 cv-qualification 不同,*this
它仍然是一个左值表达式。