万能引用与完美转发

一、universal references(通用引用)

当右值引用和模板结合的时候T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。例如:

1 template<typename T>
2 void f( T&& param){
3     
4 }
5 f(10);  //10是右值
6 int x = 10; //
7 f(x); //x是左值
如果上面的函数模板表示的是右值引用的话,肯定是不能传递左值的,但是事实却是可以。这里的&&是一个未定义的引用类型,称为universal references,它必须被初始化,它是左值引用还是右值引用取决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个universal references
例如:
 1 template<typename T>
 2 void f( T&& param); //这里T的类型需要推导,所以&&是一个 universal references
 3 
 4 template<typename T>
 5 class Test {
 6   Test(Test&& rhs); //Test是一个特定的类型,不需要类型推导,所以&&表示右值引用  
 7 };
 8 
 9 void f(Test&& param); //右值引用
10 
11 //复杂一点
12 template<typename T>
13 void f(std::vector<T>&& param); //在调用这个函数之前,这个vector<T>中的推断类型
14 //已经确定了,所以调用f函数的时候没有类型推断了,所以是右值引用
15 
16 template<typename T>
17 void f(const T&& param); //右值引用
18 // universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效
所以最终还是要看T被推导成什么类型,如果T被推导成了string,那么T&&就是string&&,是个右值引用,如果T被推导为string&,就会发生类似string& &&的情况,对于这种情况,c++11增加了引用折叠的规则,总结如下:
  • 所有的右值引用叠加到右值引用上仍然使一个右值引用。
  • 所有的其他引用类型之间的叠加都将变成左值引用。
 1 #include <iostream>
 2 #include <type_traits>
 3 #include <string>
 4 using namespace std;
 5 
 6 template<typename T>
 7 void f(T&& param){
 8     if (std::is_same<string, T>::value)
 9         std::cout << "string" << std::endl;
10     else if (std::is_same<string&, T>::value)
11         std::cout << "string&" << std::endl;
12     else if (std::is_same<string&&, T>::value)
13         std::cout << "string&&" << std::endl;
14     else if (std::is_same<int, T>::value)
15         std::cout << "int" << std::endl;
16     else if (std::is_same<int&, T>::value)
17         std::cout << "int&" << std::endl;
18     else if (std::is_same<int&&, T>::value)
19         std::cout << "int&&" << std::endl;
20     else
21         std::cout << "unkown" << std::endl;
22 }
23 
24 int main()
25 {
26     int x = 1;
27     f(1); // 参数是右值 T推导成了int, 所以是int&& param, 右值引用
28     f(x); // 参数是左值 T推导成了int&, 所以是int&&& param, 折叠成 int&,左值引用
29     int && a = 2;
30     f(a); //虽然a是右值引用,但它能取地址,本质上还是一个左值, T推导成了int&
31     string str = "hello";
32     f(str); //参数是左值 T推导成了string&
33     f(string("hello")); //参数是右值, T推导成了string
34     f(std::move(str));//参数是右值, T推导成了string
35 }

输出如下:

[root@VM-16-4-opencloudos universal_references]# ./main 
int
int&
int&
string&
string
string

所以,归纳一下, 传递左值进去,就是左值引用,传递右值进去,就是右值引用。如它的名字,这种类型确实很"通用",下面要讲的完美转发,就利用了这个特性。

二、std::forward

在讲完美转发前,先来看看实现完美转发所需用到的组件:std::forward。

源码总览:

 

 可以看到在源码中,函数 std::forward 有两种实现,差别在于传参的类型,前者接收的传参是一个左值,而后者接收的传参是一个右值。代入不同的类型来化简 std::forward 函数。当模板类型 _Tp 为 string& 时,即 string 的左值引用时,std::forward 可以化简为如下形式:
 1    //string& && 引用折叠为 string&
 2     //std::remove_reference 提取出的类型为string
 3     string&
 4     forward(string& __t)
 5     { return static_cast<string&>(__t); }
 6 
 7     string&
 8     forward(string&& __t)
 9     {
10       //此处静态断言忽略
11       return static_cast<string&>(__t);
12     }
而当模板类型 _Tp 为 string&& 时,即 string 的右值引用时,std::forward 可以化简为如下形式:
 1     //string&& && 引用折叠为 string&&
 2     //std::remove_reference 提取出的类型为string
 3     string&&
 4     forward(string& __t)
 5     { return static_cast<string&&>(__t); }
 6 
 7     string&&
 8     forward(string&& __t)
 9     {
10       //此处静态断言忽略
11       return static_cast<string&&>(__t);
12     }
可以看到,当传入类型为左值引用时,返回值为 string& : return static_cast<string&>(__t);;当传入类型为右值引用时,返回值为 string&& : return static_cast<string&&>(__t);。简单点来讲就是传入左值返回还是左值,传入右值返回还是右值,保持原来的值属性不变。std::forward 就相当于一个转发点,可以将类型原封不动的转发走。需要注意的是,std::forward只有在模板编程中结合模板参数推导和引用折叠规则使用时才有意义。

三、完美转发

完美转发是C++11引入的一项功能,旨在保持参数在转发过程中的左值或右值属性不变,从而提高函数调用的效率和灵活性。具体来说,当在模板函数或类模板中将参数传递给另一个函数时,原参数可能是左值,可能是右值,如果还能继续保持参数的原有特征,那么它就是完美的。
 1 void process(int& i){
 2     cout << "process(int&):" << i << endl;
 3 }
 4 void process(int&& i){
 5     cout << "process(int&&):" << i << endl;
 6 }
 7 
 8 void myforward(int&& i){
 9     cout << "myforward(int&&):" << i << endl;
10     process(i);
11 }
12 
13 int main()
14 {
15     int a = 0;
16     process(a); //a被视为左值 process(int&):0
17     process(1); //1被视为右值 process(int&&):1
18     process(move(a)); //强制将a由左值改为右值 process(int&&):0
19     myforward(2);  //右值经过forward函数转交给process函数,却称为了一个左值,
20     //原因是该右值有了名字  所以是 process(int&):2
21     myforward(move(a));  // 同上,在转发的时候右值变成了左值  process(int&):0
22 }

上面的例子就是不完美转发,而std::forward正是用来实现完美转发的组件之一。将上面的myforward()函数简单改写一下:

1 void myforward(int&& i){
2     cout << "myforward(int&&):" << i << endl;
3     process(std::forward<int>(i));
4 }
5 
6 myforward(2); // process(int&&):2
上面修改过后还是不完美转发,myforward()函数能够将右值转发过去,但是并不能够转发左值。
我们还需要另一个组件,就是万能引用(universal references),两者结合即可共同实现完美转发。例子如下:
 1 #include <iostream>
 2 #include <cstring>
 3 #include <vector>
 4 using namespace std;
 5 
 6 void RunCode(int &&m) {
 7     cout << "rvalue ref" << endl;
 8 }
 9 void RunCode(int &m) {
10     cout << "lvalue ref" << endl;
11 }
12 void RunCode(const int &&m) {
13     cout << "const rvalue ref" << endl;
14 }
15 void RunCode(const int &m) {
16     cout << "const lvalue ref" << endl;
17 }
18 
19 // 这里利用了universal references,如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
20 template<typename T>
21 void perfectForward(T && t) {
22     RunCode(forward<T> (t));
23 }
24 
25 template<typename T>
26 void notPerfectForward(T && t) {
27     RunCode(t);
28 }
29 
30 int main()
31 {
32     int a = 0;
33     int b = 0;
34     const int c = 0;
35     const int d = 0;
36 
37     notPerfectForward(a); // lvalue ref
38     notPerfectForward(move(b)); // lvalue ref
39     notPerfectForward(c); // const lvalue ref
40     notPerfectForward(move(d)); // const lvalue ref
41 
42     cout << endl;
43     perfectForward(a); // lvalue ref
44     perfectForward(move(b)); // rvalue ref
45     perfectForward(c); // const lvalue ref
46     perfectForward(move(d)); // const rvalue ref
47 }
上面的代码测试结果表明,在universal referencesstd::forward的合作下,能够完美的转发这4种类型。

 

 

posted @ 2024-09-14 09:22  阿玛尼迪迪  阅读(23)  评论(0编辑  收藏  举报