完美转发
1 概念
首先解释一下什么是完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。
template<typename T>
void function(T t) {otherdef(t);
}
function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。
2 C++98实现完美转发
-
C++98通过左值引用和常量左值引用+函数重载实现完美转发
-
C++98/03 标准下的 C++ 也可以实现完美转发,只是实现方式比较笨拙。通过前面的学习我们知道,C++ 98/03 标准中只有左值引用,并且可以细分为非 const 引用和 const 引用。其中,使用非 const 引用作为函数模板参数时,只能接收左值,无法接收右值;而 const 左值引用既可以接收左值,也可以接收右值,但考虑到其 const 属性,除非被调用函数的参数也是 const 属性,否则将无法直接传递。
-
如果使用 C++ 98/03 标准下的 C++ 语言,我们可以采用函数模板重载的方式实现完美转发
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {cout << "lvalue\n";
}
void otherdef(const int & t) {cout << "rvalue\n";
}
//重载函数模板,分别接收左值和右值
//接收右值参数
template <typename T>
void function(const T& t) {otherdef(t);
}
//接收左值参数
template <typename T>
void function(T& t) {otherdef(t);
}
int main()
{function(5);//5 是右值int x = 1;function(x);//x 是左值return 0;
}
// 程序执行结果为:
// rvalue
// lvalue
从输出结果中可以看到,对于右值 5 来说,它实际调用的参数类型为 const T& 的函数模板,由于 t 为 const 类型,所以 otherdef() 函数实际调用的也是参数用 const 修饰的函数,所以输出“rvalue”;对于左值 x 来说,2 个重载模板函数都适用,C++编译器会选择最适合的参数类型为 T& 的函数模板,进而 therdef() 函数实际调用的是参数类型为非 const 的函数,输出“lvalue”。
3 C++ 11实现完美转发。
- C++ 11 标准中允许在函数模板中使用右值引用来实现完美转发
万能引用规则
-
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)
-
在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可
template <typename T>
void function(T&& t) {otherdef(t);
}
引用折叠规则
-
此模板函数的参数 t 既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参。
-
C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。
int n = 10;
int & num = n;
function(num); // T 为 int&
int && num2 = 11;
function(num2); // T 为 int &&
Foward模板函数
- 引入了一个模板函数 forword(),我们只需要调用该函数,将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数呢?
#include <iostream>
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t) {cout << "lvalue\n";
}
void otherdef(const int & t) {cout << "rvalue\n";
}
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {otherdef(forward<T>(t));
}
int main()
{function(5);int x = 1;function(x);return 0;
}
// 程序执行结果为:
// rvalue
// lvalue
- 总的来说,在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。由此即可轻松实现函数模板中参数的完美转发。
1 问题定义
- 完美转发就是将函数实参以其原本的值类别转发出去。转发值类别
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename /*T*/> void bar(/*T*/ x) { /*call foo with x*/ }int main() {int i;bar(i); // expecting output: lvaluebar(1); // expecting output: rvalue
}
- 在这里,变量 i 经历了两次转发,所以我们需要先后解决这两次转发的值类别问题。
- 用户调用 bar 时,参数的值类别问题当用户以左值表达式调用 bar 时,确保其实例化(Instantiation)的形参类型为左值引用当
- 用户以右值表达式调用 bar 时,确保其实例化的形参类型为右值引用。
- bar 调用 foo 时,参数的值类别问题
- 当 bar 的形参类型为左值引用时,将其以左值转发给 foo
- 当 bar 的形参类型为右值引用时,将其以右值转发给 foo
C++ 通过转发引用来解决第一个匹配,通过 std::forward 来解决第二个匹配。
2 转发引用
原理
- 转发引用基于一个叫做引用坍缩(Reference Collapsing)的原理:
rvalue reference to rvalue reference collapses to rvalue reference, all other combinations form lvalue reference.typedef int& lref;
typedef int&& rref;
int n;
lref& r1 = n; // type of r1 is int& , int& + & => int &
lref&& r2 = n; // type of r2 is int& , int& + && => int &
rref& r3 = n; // type of r3 is int& , int&& + & => int &
rref&& r4 = 1; // type of r4 is int&&, int&& + && => int &&
举例说明
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x); // x is a forwarding referenceint main() {int i = 1;int &lref = i;int &&rref = 1;bar(1); // T 为 int, decltype(x) 为 int&&bar(i); // T 为 int&, decltype(x) 为 int&bar(lref); // T 为 int&, decltype(x) 为 int&bar(rref); // T 为 int&, decltype(x) 为 int&bar(std::move(rref)); // T 为 int, decltype(x) 为 int&&bar<int &&>(1); // T 为 int&&, decltype(x) 为 int&&
}
第一步转发问题解决
- 解决了调用 bar 时参数值类别的问题,现在我们将 bar 的参数传递给
foo:void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x) { foo(x); }
3 forward转发
forward原理
- std::forward 的实现如下:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"" substituting _Tp is an lvalue reference type");return static_cast<_Tp&&>(__t);
}
- 实现了将左值转发为左值或右值,将右值转发为右值。
举例说明
void foo(const int &) { std::cout << "lvalue" << std::endl; }
void foo(const int &&) { std::cout << "rvalue" << std::endl; }int main() {int i = 1;foo(std::forward<int>(i)); // output: rvalue; forward lvalue -> rvaluefoo(std::forward<int&>(i)); // output: lvalue; forward lvalue -> lvaluefoo(std::forward<int&&>(i)); // output: rvalue; forward lvalue -> rvaluefoo(std::forward<int>(1)); // output: rvalue; forward rvalue -> rvaluefoo(std::forward<int&>(1)); // error: static_assert failed due to requirement '!is_lvalue_reference<int &>::value' "can not forward an rvalue as an lvalue"foo(std::forward<int&&>(1)); // output: rvalue; forward rvalue -> rvalue
}
第二步转发的实现
void foo(int &) { std::cout << "lvalue" << std::endl; }
void foo(int &&) { std::cout << "rvalue" << std::endl; }
template<typename T> int bar(T &&x) { foo(std::forward<T>(x)); }int main() {int i = 1;bar(i); // output: lvaluebar(1); // output: rvalue
}
- 我们总结一下。完美转发问题是将函数的参数以其原本值类别转发出去的问题。转发引用 和 std::forward 共同解决了完美转发问题。其中,转发引用将函数的左值实参推导为左值引用类型,右值实参推导为右值引用类型。std::forward 将左值引用类型的实参转发为左值,将右值引用类型的实参转发为右值。