C++11之右值引用和移动语义
左值和右值
(小补充:为什么要设计有左值还要右值?因为可以在一些地方进一步提高效率)
• 左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中**,我们可以获取它的地址**,左值可以出现赋值符号的左边(可以修改),也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
Type& x 左值引用
Type&& y 右值引用
• 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边(不能修改),右值不能取地址。
//左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';//这个函数返回的是引用,所以是左值
cout << &c << endl;
cout << (void*)&s[0] << endl;//这里我们是想要打印地址,而s[0]是char类型,&s[0]是char*,而cout一个char*会遇到'/0'停止,所以为了能打印地址,我们要强转成void*
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);//传值返回 临时对象是右值
string("11111");//匿名对象 也是右值
//对以上右值进行比如cout<<&(x+y)<<endl;会全部报错,没有一个能取地址的。并且报错会明确说:"&"要求左值
所以,总结常见的右值,有3类:字面量常量,表达式,函数调用中间产生的临时对象,匿名对象
• 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value的缩写。现代C++中,lvalue被解释为loacte value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽rvalue被解释为read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
左值引用和右值引用
引用就是取别名
• Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。(x是左值,y是右值)
//左值引用、右值引用示例:
//左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
//右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x+y);
string&& rr4 = string("11111");
左右值引用的交叉(可以交叉,但有条件)
• 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值。
右值都具有常性吗?
右值通常具有 “类似常量” 的特性。
右值通常只能出现在赋值运算符的右侧,这使得它们在使用上有点类似常量,因为它们一般不会在其生命周期内被修改。从这个角度看,右值在大多数情况下表现出了 “常性” 的特征。
所以这也是为什么我们建议形参使用const修饰,因为这样的话就可以左右值都可以传过来了:
(小补充:那么,为什么不写一个右值引用的版本呢?因为右值引用是c++11才有的,如果不支持c++11的,那就不能用右值引用,但是为了能支持右值也传参过来,所以写const修饰的左值引用是最合适的)
• 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值) (相当于用move把左值的属性转换成右值)
move(有些复杂),是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识,这个之后再说。
move的底层原理其实很简单,就是强转。
左值引用右值引用也是可以强转的:
string&& rrx5 = (move)s;//s是左值
//上面就等同于:
string&& rrx5 = (string&&)s;//强转
看int&& rrx1 = move(b);
这里b本来是左值,被move之后b是左值还是右值?
答案是,b仍然是左值,只是move(b)是右值。(因为move底层就是强制类型转换)
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
注意强制类型转换不会改变对象本身的属性,比如b最后还是左值。
• 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
int&& rr1=10;
//所以在下面这句中,rr1的属性是左值,不能再被右值绑定,除非move一下
int& r6 = rr1;//可以被左值绑定
int&& rrx6 = move(rr1);//如果想再被右值绑定,需要move一下
这个点我们一会再解释。
• 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1 汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
语法是语法,底层是底层。
从汇编来看,底层都是指针,所以可以互相转换。底层都是一样的,只是语法层面的解释不一样。
引⽤延⻓⽣命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
曾经我们说过,临时对象和匿名对象的生命周期都只在当前这一行。
const std::string& r2 = s1 + s1;
那么,s1+s1的临时对象的生命周期就跟着r2走了,r2销毁,这个临时对象才会销毁。
右值引用也可以延长生命周期:std::string&& r3 = s1 + s1;
左值和右值的参数匹配
• C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
• C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const左值引⽤),实参是右值会匹配f(右值引⽤)。
• 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉很怪,接下来学习讲右值引⽤的使⽤场景时,就能体会这样设计的价值了
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调⽤ f(int&)
f(ci); // 调⽤ f(const int&)
f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
// 右值引⽤变量在⽤于表达式时是左值
int&& x = 1;
f(x); // 调⽤ f(int& x) ,奇怪的现象?因为右值引用本身的属性是左值
f(std::move(x)); // 调⽤ f(int&& x)
return 0;
}
(编译器的原则一直是去调用最匹配的那个)
右值引用可以去引用右值,但是右值引用本身的属性是左值
所以在上面说的“奇怪的现象”里,x是左值,可以修改。
所以右值引用的一个很大意义就在于:右值本身不能修改,但是被右值引用引用之后就可以修改了。
右值引用和移动语义的使用场景
左值引用主要使用场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传 左值引⽤返回(因为返回的是临时对象,出了作用域就销毁了,返回其别名相当于野指针了),C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法改变对象已经析构销毁的事实。
虽然说左值引用右值引用可以给临时对象和匿名对象“续命”。但如果一个函数是将局部对象返回,无论是左值引用还是右值引用都无法给其续命,因为这个临时对象会跟着函数栈帧销毁。所以只能用传值返回,那怎么办呢?
移动构造和移动赋值
• 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引 ⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
• 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函 数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
• 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有 意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的 右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
void swap(string& ss)
{
::swap(_str,ss._str);
::swap(_size,ss._size);
::swap(_capacity,ss._capacity);
}
//拷贝构造
string(const string& s)
:_str(nullptr)
{
cout<<"string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for(auto ch : s)
{
push_back(ch);
}
}
//移动构造
string(string&& s)
{
cout<<"string(const string& s) -- 拷贝构造" <<endl;
//转移掠夺你的资源(反正你也不要了等下也要销毁)
swap(s);//右值引用本身的属性是左值,所以可以swap
}
所以现在是把str的资源直接给ret,也相当于是减少了拷贝,str该销毁销毁。
所以不是借助右值引用搞事情,而是借助右值引用形成的移动构造搞事情。
momo::string s1("111111111111111111");
momo::string s3 = s1;//拷贝构造
momo::string s4 = momo::String("22222222222");//匿名对象(右值)
momo::string s5 = move(s1);
所以左值不要轻易去move,会导致资源被人抢走。
所以move是需要小心使用的。
移动构造的代价是很低的,因为不用去开空间去拷贝。拷贝构造的代价更大。