目录
不能取地址的就是右值。例如:字面常量、临时变量。
- //1就是右值
- int i = 1;
- //max返回值是临时变量,也就是右值
- int n = max(1, 2);
左值引用是对左值的引用,顾名思义,右值引用就是对右值的引用。
右值引用符号为&&,使用方式与左值引用相同,但符号和引用对象属性不同。
左值引用 | 右值引用 | |
符号 | & | && |
引用对象 | 左值(可取地址的变量) | 右值(不可取地址) |
使用方式 | int j = 1; int& a = j; | int&& a = 1; |
左值和右值无法直接传递。
- int main()
- {
- int a = 1;
- int& b = a;//正确,左值引用
-
- int&& c = b;//错误。右值引用不能传左值
-
- int& d = 1;//错误。左值引用不能传右值
- return 0;
- }
该函数参数为左值,返回值为右值。
作用是将左值参数强制转化成右值引用。
- int a = 1;
- int&& c = move(a);//将a强转成右值
左值引用加上const修饰符即可。
const int& d = 1;
右值引用本身是左值属性。
因此,如果给左值引用传递右值引用是可以的。
- int&& a = 1;//a为右值引用,但自身属性是左值
- int& b = a;//正确,给左值引用传递左值
这该怎么理解呢?
《C++ Primer》对此给出了相关解释,大意如下:
左值是“持久”的,右值是“短暂”的。即左值只要不出作用域就可以一直存在,但右值只能在使用时的瞬间“存活”(参考函数返回值)。
因此,当进行右值引用后,引用本身可以一直在作用域中存在,那么它就是左值。
当然,可以使用另一种方式证明:取地址。
左值可以取地址,右值不可以取地址。
- int main()
- {
- int a = 1;//a可以取地址,是左值
- int&& b = 3;
- cout << "a地址: " << &a << endl;
- cout << "b地址: " << &b << endl;
- return 0;
- }
不妨总结一下:
左值 | 右值 | 左值引用 | 右值引用 | |
---|---|---|---|---|
举例 | int a = 1; | string str = "abc"; | int& b = a; | int&& c = 1; |
属性 | 左值 | 右值 | 左值 | 左值 |
取地址 | 能 | 不能 | 能 | 能 |
转化 | 接收右值: 直接传 | 接收左值: 无 | 接收右值: +const | 接收左值: move函数 |
C++11引入右值引用后,很大的作用便是移动构造与赋值。
比如官方STL库中就提供了相关函数:
移动构造的目的在于减少因为参数是左值时引发的重复拷贝的问题。
以string为例进行说明:
截取如下代码:
- class String {
-
- ...
-
- explicit String(const char* a = "")//默认构造
- {
- _size = strlen(a);
- _capacity = _size;
- _a = new char[_capacity + 1];
- strcpy(_a, a);
- cout << "构造函数\n";
- }
-
- String(const String& st)//拷贝构造
- :_a(nullptr)
- {
- String tmp(st.c_str());//调用构造函数
- swap(tmp);
- cout << "拷贝构造\n";
- }
-
- ...
-
-
- };
- String To_string(int value)//将int转为string
- {
- ...
- String str;
- ...
- return str;
- }
- int main()
- {
- String str = To_string(20);
- return 0;
- }
当我们执行这个程序时,会调用2个默认构造和1个拷贝构造:
分别是to_string内部生成str时调用默认构造、返回临时变量时调用string拷贝构造,但是string拷贝构造内部又会先调用默认构造。
其实这还是优化后,如果没有编译器优化,main函数中str也会再调一次string拷贝构造。
而这一切的“罪魁祸首”是什么呢?——to_string的返回值。
是的,因为to_string内部会生成一个string对象,而该对象是局部变量,出了函数作用域就销毁,因此只能调用拷贝构造to_string内部的对象。
这还只是string类型拷贝构造,如果是更加复杂的类型,拷贝构造往往会造成更多资源的占用。
正因如此,移动构造派上了用场:
- String(String&& st)//移动构造函数,但是参数为右值
- :_a(nullptr)
- {
- swap(st);
- cout << "string移动构造\n";
- }
移动构造的参数为右值,所以当to_string返回str时,会被移动构造接收。
虽然str本身为左值属性,但是因为此时str是“将亡值”,即出了函数作用域就会被销毁,编译器会将这种“即将死亡”的值识别为右值。
在移动构造内部,会将右值的数据与自身数据进行交换。因为右值作为“暂时存在的数据”,把数据交给目标对象,目标对象把“舍弃”的数据交给右值,正好可以“延续”目标数据且消除原本数据。
这时,接收to_string返回值时只需要一个一个移动构造即可:
移动赋值的目的与移动构造类似,在于减少因为赋值造成重复拷贝的问题。
以string为例,其中赋值重载通过调用了拷贝构造函数实现。
- class String {
-
- ...
-
- String& operator=(const String& st)//赋值重载1
- {
- String tmp(st);//调用拷贝构造
- swap(tmp);
- cout << "string赋值\n";
- return *this;
- }
- String& operator=(const char* str)//赋值重载2
- {
- String tmp(str);
- swap(tmp);
- cout << "char*赋值\n";
- return *this;
- }
-
- ...
-
-
- };
- int main()
- {
- String str;
- cout << "--------------------------------\n";
- str = To_string(1);
- return 0;
- }
当执行这个程序时,会有多个构造、拷贝构造被调用:
而这其中,属于因为赋值重载而调用的就有三个。
因为赋值重载的参数是左值引用,不能像右值引用那样交换数据,只能调用拷贝构造获取数据。
由此,移动赋值应运而生:
与移动构造相同,移动赋值也是直接与右值交换数据。
- String& operator=(String&& st)
- {
- swap(st);
- cout << "string移动赋值\n";
- return *this;
- }
此时,只需要将to_string的返回值作为右值传给移动赋值即可。
首先,万能引用只存在与模板编程中。
万能引用就是引用形参既可接收左值也可接收右值,其符号与右值引用相同,但必须是模板。
即当模板的参数是右值引用的形式,如果实参是左值就是左值引用,右值就是右值引用。
例如下列代码:
- void Print(int& a)
- {
- cout << "左值" << endl;
- }
-
- void Print(int&& a)
- {
- cout << "右值" << endl;
- }
-
- template<class T>
- void func(T&& t)//万能引用
- {
- Print(t);
- }
-
- int main()
- {
- int a = 0;
- func(a);//传左值
- func(1);//传右值
- return 0;
- }
上述代码有一个问题,尽管func(1)传入的是右值,但是因为右值引用本身是左值,当调用Print函数时,会调用左值版本,这不符合我们的预期,因为明明传入的是右值:
这时,就需要使用完美转发forward,它会保持传入实参的属性不变:
- void func(T&& t)
- {
- Print(std::forward
(t)); - }
首先看一下move函数底层代码:
- template <typename T>
- typename remove_reference
::type&& move(T&& t) - {
- return static_case<typename remove_reference
::type&&>(t); - }
其中参数T&&是万能引用,可接收左值或右值。
返回值很特殊,typename remove_reference
remove_reference本身是模板类,它的作用就是返回一个类型,所以这个类里面只有成员类型。
通过remove_reference源码可以看到,不管传入的是左值引用还是右值引用,它都只会返回这个值去掉引用后的类型。
我们以int为例,不管传入int&还是int&&,经过remove_reference
- template <typename T>
- struct remove_reference{
- typedef T type; //成员类型
- };
-
- template <typename T>
- struct remove_reference
//左值引用 - {
- typedef T type;//返回T本身的类型
- }
-
- template <typename T>
- struct remove_reference
//右值引用 - {
- typedef T type;//返回T本身的类型
- }
static_case作用是强制类型转换,可以将左值强转成右值,move中是强转成右值引用。
因此,move底层代码可以翻译成如下形式:
- template <typename T>
- int&& move(T&& t)
- {
- return (int&&)(t);
- }
于是,我们清楚的发现:move函数就是通过remove_reference获取引用对象本身的类型,强转成右值引用的方式实现的。
这是forward底层代码:
- template <typename T>
- T&& forward(typename std::remove_reference
::type& param) //左值引用 - {
- return static_cast
(param);//万能引用 - }
-
- template <typename T>
- T&& forward(typename std::remove_reference
::type&& param) //右值引用 - {
- return static_cast
(param);//万能引用 - }
有了move的基础,forward就不难理解了。
它通过remove_reference来区分传入的参数是左值引用还是右值引用,然后调用具体的重载forward函数。
再通过万能引用的形式,根据param的具体类型返回左值引用还是右值引用。
源码可以翻译成如下形式(int为例):
- template <typename T>
- T&& forward(int& param)//左值引用
- {
- return (T&&)(param);//万能引用
- }
-
- template <typename T>
- T&& forward(int&& param)//右值引用
- {
- return (T&&)(param);//万能引用
- }
参考文章:
聊聊C++中的完美转发 - 知乎 (zhihu.com)
C++高阶知识:深入分析移动构造函数及其原理 | 音视跳动科技 (avdancedu.com)
参考书籍:
《C++ Primer》
程序是我的生命,但我相信爱她甚过爱我的生命。——未名
如有错误,敬请斧正