右值引用
传统的C++引用(左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可以获得其地址。
C++11新增了右值引用。右值引用,顾名思义,可以关联到右值,即——可以出现在赋值表达式的右边,但不能对其应用地址运算符的值。
右值包括字面常量(C风格字符串除外,它表示地址)。诸如x+y等表达式以及返回值的函数(条件是该函数返回的不是引用)。
右值引用用符号&&表示。如:
int x = 10, y = 23;
int & r = x + y;//非法,编译器报错
int && r2 = x + y;//合法
double && r3 = std::sqrt(2.0);
将右值关联到右值引用导致该右值被存储到特定的位置,且可以获取该位置的地址。也就是说,虽然不能将运算符&用于12或者(x + y),但可以将其用于r1。通过将数据与特定的地址关联,使得可以通过右值引用来访问该数据。
但其实这不是C++11引入右值引用的主要原因。
移动语义
有些函数,表达式的执行会创建出临时对象,在临时对象即将被销毁的时候,将字符,类对象等变量留在原来的地方,不去删除它们,而是改变它们的所有权,使得它们仍然储存在原来的地址上,但摆脱了临时对象的身份,这种方法被称为移动语义。
例如有一个函数返回一个临时变量,此时编译器做的工作就是,创建一个该临时对象的副本,然后将临时对象销毁,再将该临时对象的副本赋值给一个普通对象,然后销毁该副本。而移动语义的做法是:直接将临时对象的所有权交给该普通对象。
其实移动语义实际上避免了移动原始数据,而只是修改了记录而已。
用右值引用实现移动语义
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。
对于类来说,可以定义一个移动构造函数。与常规的复制构造函数(使用左值引用,而且一般声明为const)不同,移动构造函数使用右值引用作为参数,该引用关联到右值实参。而且不同与复制构造函数可执行的深复制,移动构造函数只是调整记录。在将所有权转移转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。
下面举例说明移动构造函数的用法及其与复制构造函数的区别:
//为了偷懒,程序只列出了两个构造函数的具体定义并说明他们的区别
#include "iostream"
using namespace std;
class example{
private:
int n; //number of elements
char * pc; //pointer to data
public:
example();
explicit example(int k);
example(int k, char ch);
//copy constructor
example(const example & f): n(f.n) {
pc = new char[n];
for (int i = 0; i < n; ++i)
pc[i] = f.pc[i];
}
//move constructor
example(example && f) : n(f.n) {
pc = f.pc;
f.pc = nullptr; //c++11
f.n = 0;
}
~example();
};
下面语句使用的是复制构造函数,它执行深复制:
example two = one; //引用f指向左值对象one
下面语句使用的是移动构造函数:
example three (one + two);
在移动构造函数的定义中,它让pc指向现有的数据,以获取这些数据的所有权。此时,因为如果pc和f.pc指向相同的诗句,调用析构函数时将带来麻烦,因为程序不能对同一个地址调用两次delete []。所以该构造函数随后将原来的指针设置为空指针。这也是不声明为const的原因。
这种夺取所有权的方式成为窃取。
简而言之,实现移动语义的两个步骤:使用右值引用告诉编译器什么时候可使用移动语义和编写移动构造函数以提供所需的行为。
所以,通过提供一个使用左值引用的复制构造函数和一个使用右值引用的构造函数,两个构造函数将初始化分成了两组。
使用左值对象初始化对象时,将使用复制构造函数,而使用右值对象初始化对象时,将使用移动构造函数。
程序员可根据需要赋予这些构造函数不同的行为。
当然,适用于构造函数的移动语义考虑也适用于赋值运算符。如
example & example::operator=(example && f) {
if (this == &f) return *this;
delete []pc;
n = f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr; //C++11
return *this;
}
移动赋值运算符删除目标对象中的原始数据,并将源对象的所有权转让给目标。
强制移动:
假设one two是两个类对象,下面的语句将调用赋值运算符:
one = two;
但是如果不想调用复制运算符而是想直接使用移动语义呢?
c++11提供了头文件utility中的move函数来进行转换:
#include<utility>
one = std::move(two);
请注意:能这样做的前提是已经定义了移动赋值运算符。