关于什么是左值、什么是右值,可以参考官方定义
https://en.cppreference.com/w/cpp/language/value_category
还有一篇文章
https://zhuanlan.zhihu.com/p/54050093
理解左值 右值
下面将对左值和右值的用法进行剖析,反向理解什么是左值、右值。
右值据有两个特点:
所引用的对象将要被销毁
该对象没有其他用户
使用右值引用代码可以自由地接管所引用的对象的资源
程序的数据一般放在内存,那么需要用到的时候,直接取用。例如
int a = 10;
当我们需要使用a这个名字所代表的内存值,通过a就可以找到这块内存。并且可以做如下操作
a = a + 1
在上面的式子中,将a的值取出到寄存器与1相加,再把相加后的值放回a中。同时,上式中a是左值,a+1是右值,可以看出,a+1的过程是在寄存器中进行的,结果也放在寄存器中。然后再把结果放回a指向的内存。下一刻,该寄存器的a+1可能又被什么运算的结果覆盖掉了。
这就是primer 5th所说的所引用的对象将要被销毁(p471)。
为了不浪费这个值(a+1),c++编译器通过右值引用的方式延长a+1(这个值在寄存器中)。例如
int &&r = a + 1;
int v = r;
int &&r2 = std::move(r);
r是a+1的右值引用,同时r是左值。
基本数据类型有左右值之分,类也有左右值。即如果可以使用右值引用绑定基本类型,那么也可以绑定类。对于类而言,存在移动构造函数和移动赋值函数。
什么时候调用移动操作,类似上面的第二个赋值操作,此时就会调用移动操作,窃取寄存器资源。所以需要实现类的移动构造函数和移动赋值函数。
掌握移动操作
右值应用最强大的地方是在类能够轻易使用右值,减少拷贝的开销
下面是移动构造函数(primer 5th p473)
StrVec::StrVec(StrVec &&s) noexcept
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
s.elements = s.first_free = s.cap = nullptr;
}
下面代码是没有意义的,只是为了说明
StrVec sv1; //(1)
StrVec sv2 = sv1; //(2)
StrVec sv3 = std::move(sv1); //(3)
这里涉及到&&s这个变量,如果用一个右值去调用StrVec构造函数(3),不会重新分配任何内存,而是直接接管s的内存。最终s(移源后的对象 sv1)被销毁了,则调用析构函数,而析构函数析构的elements/first_free/cap都被置为nullptr,不会影响sv3。
但是这里需要注意,因为sv1使用了move,将sv1左值转换为右值使用,除了对sv1赋值和销毁(离开作用域),不能使用sv1的值。即可以赋予它新值,但是不能使用移源后对象的值。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if (this!= &rhs) {
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = cap = nullptr;
}
return *this;
}
移动构造和移动赋值都是将移源后的对象置于与默认初始化的对象相同的状态。
没有移动操作,对于右值对象的拷贝和赋值操作,都会调用最普通的拷贝构造和赋值操作。有了移动操作,主要有哪些应用场景呢?
1、函数返回的是一个右值,就可以调用移动操作
class HasPtr {
public:
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;}
HasPtr& operator=(HasPtr rhs)
{ swap(*this, rhs); return *this; }
}
在类代码小心使用move,可以大幅度提升性能。但是一个移源后对象具有不确定的状态,对其调用std::move是危险的。调用move时,必须绝对确认移源后对象没有其他用户