参考:《Primer C++》、《Effective Modern C++》
①移动语义使得编译器得以使用不那么昂贵的移动操作,来替换昂贵的复制操作。同复制构造函数、复制赋值运算符给予人们控制对象复制的具体意义的能力-样,移动构造函数和移动赋值运算符也给予人们控制对象移动语义的能力。移动语义也使得创建只移型别对象成为可能,这些型别包括 std::unique_ptr
、std::future
和 std::thread
等。
②完美转发使得人们可以摆写接受任意实参的函数模板,并将其转发到其他函数目标函数会接受到与转发函数所接受的完全相同的实参。
当一个对象被用作右值的时候,用的是对象的值内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
不同的运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运算对象;返回值也有差异,有的得到左值结果、有的得到右值结果。一个重要的原则是在需要右值的地方可以用左值来代替,但是不能把右值当成左值也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的。
使用关键字 decltype 的时候,左值和右值也有所不同。如果表达式的求值结果是左值,dec1type 作用于该表达式(不是变量)得到一个引用类型.举个例子,假定 p 的类型是 int*
,因为解引用运算符生成左值,所以 decltype(*p)
的结果是 int&
。另一方面,因为取地址运算符生成右值,所以 dec1type(&p)
的结果是 int**
,也就是说,结果是一个指向整型指针的指针。
些表达式生成或要求左值,而另外一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用为了与右值引用区分开来,我们可以称之为**左值引用 (lvalue reference)**我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。
int i =42;
int&r = i; //正确:r引用i
int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i *42; //错误:1*42是一个右值
const int &r3 = i * 42; //正确:我们可以将一个const的引用绑定到一个右值上
int && rr2 = i * 42; //正确:将r2绑定到乘法结果上
考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态,而右值要么是字面常量,要么是在表达式求信过程中创建的临时对象
由于右值引用只能绑定到临时对象,我们得知:
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的
结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上,这有些令人惊讶
int &&rr1 =42; //正确:字面常量是右值
int &&rr2 =rr1; //错误:表达式 rr1 是左值!
其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。
那我们应该怎么办呢?使用 std::move
获得绑定到左值上的右值引用。
int &&rr3 = std::move(rr1);
move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对r1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。