目录
一、前言
二、概念
2.1 左值和左值引用
2.2 右值和右值引用
三、左值引用与右值引用的区别
四、应用场景
4.1 左值引用的短板(将亡值)
4.2 右值引用的意义(移动语义)
五、右值引用引用左值
六、万能引用和完美转发
一、前言
在传统的C++语法中就有引用,而C++11中又新增了右值引用的语法特性,从此传统的引用就变为了左值引用。无论是哪种引用,都是给一个对象取别名,不过二者针对的对象种类不同
右值引用是一块晦涩难懂的语法糖,何为左值?何为右值?何为左值引用?何为右值引用?
二、概念
2.1 左值和左值引用
左值(lvalue)是一个表达式,它表示一个可被标识的(变量或对象的)内存位置,并且允许使用&操作符来获取这块内存的地址。如果一个表达式不是左值,那它就被定义为右值。
简单来说,左值就是可以出现在赋值符号左侧的值,一个表示数据的表达式(如变量名或指针)就是左值;可以被取地址的就是左值;可以被赋值的就是左值。
而左值引用很好理解,就是对左值的引用,符号为&
例如:
int main()
{
int a = 1; //a是左值
int* p = new int(1); //*p和p是左值
const int b = 1; //b是左值
int& ra = a; //左值引用
int*& rp = p; //左值引用
const int& rb = b; //左值引用
return 0;
}
2.2 右值和右值引用
一个表达式如果不是左值,那么就是右值。如字面常量、表达式返回值、非左值引用返回的函数返回值等都是右值。
右值可以出现在赋值符号的右边,但是一定不能出现在赋值符号的左边。右值不能被取地址
右值引用就是对右值的引用,符号为&&
例如:
int func(int x, int y)
{
return min(x, y);
}
int main()
{
//右值与右值引用
int&& r1 = 1;
double&& r2 = 1.1 + 2.2;
int&& r3 = func(1, 2);
//右值不能出现在赋值符号左侧
10 = 1; //报错
func(1, 2) = 3; //报错
return 0;
}
需要注意,右值虽然不能被取地址,但是右值被引用后会存储到特定位置,可以取到该位置的地址并用这个地址对其进行修改。
虽然看上去很矛盾,但是这是有必要的,因为后面提到的移动构造和移动赋值中就需要对右值进行修改。由此我们也可以得出结论:右值被右值引用引用以后的属性是左值
如果不想被修改,可以用const右值引用。
int main()
{
//右值与右值引用
int&& r1 = 1;
const double&& r2 = 1.1 + 2.2;
r1 = 10;
r2 = 3.3; //报错
return 0;
}
三、左值引用与右值引用的区别
左值引用只能引用左值,不能引用右值;但const左值引用既可以引用左值也可以引用右值
int main()
{
//左值引用
int& r1 = 1; //报错
//const左值引用
const int& r3 = 1;
return 0;
}
像1这种常量是具有常性的,无法被左值引用,否则会造成权限放大,加上const就不会权限放大了
同样,右值引用只能引用右值,不能引用左值;但右值引用可以引用move后的左值
何为move后的左值?我们可以对一个左值进行move操作将其转换为右值,例如:
int main()
{
int a = 10; //左值
//右值引用
int&& r1 = a; //报错
//右值引用引用move后的左值
int&& r2 = move(a);
return 0;
}
关于move函数在后面会进行详细讲解
四、应用场景
4.1 左值引用的短板(将亡值)
既然const左值引用既可以引用左值又可以引用右值,那我们为什么还需要右值引用呢?
实际上左值引用在某些情况下依然存在短板,需要右值引用才能解决
我们都知道,在函数的参数和返回值中使用引用可以提高效率。但是当函数的返回对象是一个局部变量,出了函数作用域就被销毁了,那么我们就不能使用左值引用返回了。例如:
像这样,ret是函数内的局部变量,出了函数作用域就被销毁,无法使用引用返回,只能使用传值返回进行拷贝构造
对于这种完成资源拷贝或转移后就被销毁的值,我们叫做将亡值。将亡值也是右值
4.2 右值引用的意义(移动语义)
对于这类将亡值,我们想要的是它的资源,但是如果还要为此进行额外的深拷贝的话就不够优雅了
因此C++11引入了移动语义,增加了移动构造和移动拷贝,本质就是直接将右值的资源占为己有,而不再需要新开空间拷贝数据,提高了效率,例如:
像上面的情况,编译器就会优先使用移动构造和移动拷贝,把将亡值的资源直接转移
移动构造和移动拷贝对于需要深拷贝的自定义类型有意义,对于只需要浅拷贝的类型来说意义不大
我们也可以自己模拟实现最基本的移动构造和移动拷贝,看看编译器是否真的会调用
//string.h
namespace Eristic
{
class string
{
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s)
:_size(s.size())
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(string&& s)
{
cout << "string operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
~string()
{
delete[] _str;
_capacity = _size = 0;
}
//...
char* _str;
size_t _size;
size_t _capacity;
};
}
//test.cpp
#include <iostream>
#include <assert.h>
using namespace std;
#include "string.h"
Eristic::string func()
{
Eristic::string ret = "hello";
return ret;
}
int main()
{
Eristic::string s;
s = func();
return 0;
}
可以看到,编译器确实会优先调用移动构造和移动赋值
不只是构造和拷贝构造,像vector的insert也有左值引用和右值引用版本
五、右值引用引用左值
前面提到,右值引用可以引用move后的左值,具体是如何实现的呢?
可以看到,a被move后还是左值,这是怎么回事?
实际上,move函数并不会把左值修改为右值,而是其返回值是一个右值
move函数的实现如下:
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
需要注意,如果我们将一个左值经过move处理后调用移动构造去构造另一个对象,原来的左值的资源就被转移给该对象了,这个左值就被置空了
六、万能引用和完美转发
模板中的&&不代表右值引用,而是万能引用,其既可以接收右值也可以接收左值,例如:
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
PerfectForward(10); //右值
int a;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 8;
PerfectForward(b); //const左值
PerfectForward(move(b)); //const右值
return 0;
}
但是,模板中的万能引用只是提供了能够同时接收左值引用和右值引用的能力。前面提到过,右值引用的属性是左值,在后续使用中它们都会退化为左值引用,所以上面代码的结果如下:
全都调用了左值引用版本
如果我们希望在传递过程中保留右值的属性怎么办呢?就需要使用接下来提到的完美转发了
forward是一个标准库函数,它能够根据传递给它的参数的类型,保持该参数的值类别(左值或右值)
如果一个参数是右值,经过forward处理后,它还是右值;如果是左值,处理后仍然是左值
完.