移动语义详解
右值引用
//通过对const,将右值绑定到左值引用上
const int &i = 123;
void m_log(const string &s){
cout << s << endl;
}
//main
m_log("hello move");
m_log函数只允许接受一个常量左值引用,"hello move"是一个右值,函数通过一个拷贝构造函数创建一个左值形参,并取它的常量引用传入函数,在此例中创建了一个左值一个右值两个变量,这是因为目标函数的形参是一个左值引用。
- 如果把目标函数的形参设为一个右值引用,那么就将节约一次创建变量的时间和空间
- 有许多类逻辑上不允许拷贝,例如unique_ptr (可以作为返回值),“所有权”是一种转移的概念,类似的还有线程所有权、IO类的IO缓冲,这些类都包含不能被共享的资源,这些类型的对象不能被拷贝但可以被移动。
- 右值所引用的对象应该是即将要被销毁的对象。
移动构造函数
可以为类创建一个以右值引用为对象的构造函数,来实现“转移的逻辑”,那么没使用一个临时变量初始化一个新的对象时都可以避免一次拷贝。
对 对象使用move过后,对象仍然存在,但对于已实现移动构造函数的类,其对象中资源已被转移。
例子:
class Person{
public:
int *data;
static const int blockSize = 1000;
string name;
Person(string s):data(new int[blockSize]){
name = s;
cout << "constructor" << endl;
}
//拷贝构造
Person(const Person& tmp):data(new int[blockSize]){ //此处要新建资源
name = tmp.name;
for(int i = 0;i < blockSize;i ++){
data[i] = tmp.data[i];
}
cout << "copy constructor" << endl;
}
//移动构造
Person(Person&& tmp) noexcept{ //此处只需要移动资源
data = tmp.data;
tmp.data = nullptr;
cout << "move constructor";
}
~Person(){
cout << "delete" << endl;
}
void m_log(){
cout << "person" << endl;
}
};
/*运行程序
vector<Person> db;
db.push_back(Person("Tom"));
cout << endl;
db.push_back(Person("Jack"));
cout << endl;
db.push_back(Person("Morty"));
cout << endl;
注销移动构造的输出
constructor
copy constructor
constructor
copy constructor
copy constructor
constructor
copy constructor
copy constructor
copy constructor
vector 的大小时按2的倍数分配的,当push第二个元素时,vector会单独申请大小为之前2倍的capacity,并把第一个元素重新拷贝进新空间,再插入第二个元素。
移动构造的输出
constructor
move constructor
constructor
move constructor
move constructor
constructor
move constructor
move constructor
move constructor
执行移动语义
noexcept
由于移动操作时“拦截窃取操作”,通常不会抛出异常。当编写一个不抛出异常的库函数时,应该通知标准库,使之不要做一些额外的操作。
在vector
重新分配空间并执行原有对象从老空间到新空间的拷贝时,vector
此时并不会默认调用移动构造函数而是拷贝构造函数,这是因为如果重新分配过程使用了移动构造函数,且在移动了部分而不是全部元素后抛出一个异常就会产生问题——旧空间的移动源元素已经改变了,而新空间中为构造的元素可能尚不存在,在此情况下,vector将不能满足自身保持不变的要求。另一方面,如果vector
调用的是拷贝构造函数,它很容易得可以保持旧元素不变且释放新分配的(但还未成功构造的)内存并返回。vector
原有的元素仍然存在。因此,如果我们希望vector
在重新分配空间时执行的是移动操作而不是拷贝,我们通过将移动构造函数标记为noexcept
来做到这一点。
右值引用与函数模板
右值引用固然是个好事,但是如果每个函数都写一个左值引用再写一个右值引用的版式,那也太麻烦了吧。没错,但是右值引用的函数模板恰恰可以解决这个问题。在使用右值引用作为函数模板的参数时和之前的用法可能不同,如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用;当提供右值时,会当作普通数据引用。看下边这个例子吧:
template<typename T> void printV(T&& t){}
当传入一个右值时,T的类型被推导为:
printV(42); // printV<int>(42) printV(3.14); // printV<double>(3.14) printV(std::string("Hello")) // printV<std::string>(std::string("Hello"))
不过,向函数传入一个左值时,
T
会被推导为一个左值引用:int i = 42; printV(i); // printV<int&>(i)
因为函数参数声明
T&&
,就是引用的引用咯(当然不是这个意思,但是这里可以强行这么理解一下),那么以上printV
的类型就相当于:printV<int&>(int&);
这就允许一个函数模板既可以接受左值也可以接受右值参数。
unique_ptr和move
unique_ptr<Person> p1(new Person("Alice"));
unique_ptr<Person> p2 = move(p1);
p1->m_log();
p2->m_log();
move语句后,用户仍然有p1的访问权限,但如果类实现了移动构造函数,p1->data == nullptr