简介...
TL;DR,您希望保留函子的 value category(r 值/l 值性质),因为这会影响 overload resolution,尤其是 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) 替换为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);
}
当调用apply_impl 和apply_impl_2 时,无论是使用右值Functor1() 还是使用左值func,输出都如预期的那样,调用了重载的调用运算符。 r 值和 l 值都调用它。在 C++03 下,这就是你所得到的,你不能基于对象的“r-value-ness”或“l-value-ness”重载成员方法。
Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
函子1 ... 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() 来调用apply_impl_2,因此将调用限定为operator() 的右值。 func,作为apply_impl_2 的命名参数,是一个右值引用,但由于它被命名,它本身就是一个左值。因此,左值 operator()(int) const& 被称为左值 func2 作为参数和右值 Functor2() 被用作参数的情况。
在apply_impl(id 7 和 8)的情况下,std::forward<F>(func)保持或保留为func 提供的参数的右值/左值性质。因此,当右值Functor2() 用作参数时,左值限定operator()(int) const& 被调用,左值func2 用作参数,右值限定operator()(int)&&。这种行为是意料之中的。
结论
使用std::forward,通过完美转发,确保我们保留func 原始参数的右值/左值性质。它保留了他们的value category。
这是必需的,std::forward 可以而且应该用于不仅仅是向函数转发参数,而且当需要使用参数时,r-value/必须保留左值性质。笔记;在某些情况下,不能或不应该保留 r-value/l-value,在这些情况下不应使用 std::forward(参见下面的反面)。
出现了许多示例,它们通过看似无辜地使用右值引用而无意中失去了参数的右值/左值性质。
编写定义明确且合理的通用代码一直很困难。随着 r 值引用的引入,尤其是引用折叠,可以更简洁地编写更好的通用代码,但我们需要更加了解所提供参数的原始性质是什么,并确保当我们在我们编写的通用代码中使用它们时,它们会被维护。
完整的示例代码可以在here找到
推论和逆向
- 这个问题的必然结果是;给定模板函数中的引用折叠,如何保持参数的右值/左值性质?答案 - 使用
std::forward<T>(t)。
- 匡威;
std::forward 能解决你所有的“通用参考”问题吗?不,它没有,在某些情况下should not be used,例如多次转发值。
完美转发的简要背景
完美转发可能有些人不熟悉,那么什么是完美转发?
简而言之,完美转发是为了确保提供给函数的参数被转发(传递)到与originally provided具有相同值类别(基本上是右值与左值)的另一个函数。它通常与可能发生reference collapsing 的模板函数一起使用。
Scott Meyers 在他的 Going Native 2013 presentation 中给出了以下伪代码来解释 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 提供了一篇关于完美转发 problem 和 solution 的精彩但密集的文章。
什么是引用限定符?
引用限定符(左值引用限定符 & 和右值引用限定符 &&)与 cv 限定符相似,因为它们(ref-qualified members)在 overload resolution 期间用于确定哪种方法打电话。他们的行为与您期望的一样; & 适用于左值,&& 适用于右值。 注意:与 cv-qualification 不同,*this 仍然是一个左值表达式。