拷贝控制3(对象移动)
在对象拷贝后就立即被销毁的情况下,移动而非拷贝对象会大幅度提升性能。以及像 IO 类或 unique_ptr 这样的类,包含不能被共享的资源,不能拷贝但可以移动。而标准库容器、string 和 shared_ptr 类既支持移动也支持拷贝
右值引用:
右值引用是 c++11 为了支持移动操作引入的。右值引用就是只能绑定到右值的引用。我们通过 && 而非 & 来获取右值引用。右值引用只能绑定到一个将要销毁的对象。因此我们可以自由地将一个右值引用的资源 “移动” 到另一个对象中
类似任何引用,右值引用也是某个对象的别名。但右值引用有着与左值引用有着完全相反的引用特性:我们可以将一个右值引用绑定到要求转换的表达式、字面常量或是返回右值的表达式,但不能将一个右值引用绑定到一个左值上:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 int i = 42; 6 int &r = i;//正确,r引用i 7 // int &&rr = i;//错误,不能将一个右值引用绑定到一个左值上 8 // int &r2 = i *42;//错误,i*42 是一个右值表达式 9 const int &r2 = i * 42;//正确,常量左值引用可以绑定右值 10 int &&rr2 = i * 42;//正确,右值引用绑定到右值上 11 12 // 右值引用类型变量本身是左值的,但其算术/关系表达式是右值的 13 // int &&cnt1 = rr2;//错误,rr2变量是左值(虽然它本身是一个右值引用) 14 int &&cnt2 = rr2 * 1;//正确,rr2*1 是一个右值表达式 15 16 int &&cnt3 = 42;//字面常量是右值 17 18 //转换表达式会生成一个临时变量,即转换表达式的返回值是右值 19 int &&cnt4 = float(i); 20 21 //右值引用可以赋值右值引用 22 cnt4 = cnt3; 23 cout << cnt4 << endl;//42 24 25 return 0; 26 }
注意:返回左值引用的函数,连同赋值、下标、解引用和前置递增 / 递减运算符都是返回左值的。而返回非引用类型的函数,连同算术、关系、位以及后置递增 / 递减运算符都生成右值
变量都是左值,右值引用类型的变量同样是左值,因此我们不能将一个右值引用绑定上一个右值引用类型的变量上
类似于左值引用,右值引用引用之间也可以赋值
左值持久,右值短暂:
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
由于右值引用只能绑定到临时对象,我们得知:
所引用的对象将要被销毁
该对象没有其它用户
这意味着:使用右值引用的代码可以自由的接管所引用的对象的资源
标准库 move 函数:
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为 move 的库函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility 中:
int cnt = 1;
int &&gg = std::move(cnt);
move 调用告诉编译器,我们有一个左值但我们希望像处理右值一样处理它。
调用 move 也意味着承若:除了对 cnt 赋值或销毁外,我们将不再使用它。在调用 move 之后,我们不能对移后源对象的值做任何假设
移动构造函数和移动赋值运算符:
移动构造函数的第一个参数必须是右值引用,其余参数都必须有默认实参
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些源对象的所有权已经归属新创建的对象:
1 StrVec::StrVec(StrVec &&s) noexcept : 2 elements(s.elements), first_free(s.first_free), cap(s.cap) { 3 //令s进入这样的状态——对其运行析构函数是安全的 4 s.elements = s.first_free = s.cap = nullptr; 5 }
注意:noexcept 通知标准库我们的构造函数不抛出任何异常
与拷贝构造函数不同,移动构造函数不分配任何新内存,它接管给定的 StrVec 中的内存。在接管内存之后,将给定对象中的指针都置为 nullptr,保证完成移动构造函数后源对象被销毁不会释放掉我们刚刚移动的内存
移动操作、标准库容器和异常:
移动构造函数通常不分配任何资源,因此移动构造操作通常不会抛出任何异常。我们应该将此事通知标准库,否则标准库会认为移动我们的类对象可能会抛出异常并为了处理这种可能性而做一些额外的工作
一种通知标准库的方法是在我们的移动构造函数中指明 noexcept。在一个构造函数中,noexcept 出现在参数列表和初始化列表的冒号之间:
1 class StrVec{ 2 public: 3 StrVec(StrVec&&) noexcept;//移动构造函数 4 ... 5 6 }; 7 8 StrVec::StrVec(StrVec &&s) noexcept : /*成员初始化器*/{ 9 /*构造函数体*/ 10 }
注意:不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
移动赋值运算符:
1 StrVec& StrVec::operator=(StrVec &&rhs) noexcept { 2 if(this != &rhs){//检测自赋值的情况 3 free();//释放已有元素 4 elements = rhs.elements; 5 first_free = rhs.first_free; 6 cap = rhs.cap; 7 rhs.elements = rhs.first_free = rhs.cap = nullptr;//将rhs置于可析构状态 8 } 9 return *this; 10 }
注意:特殊处理自赋值的情况
不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept
在移动操作后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设
合成的移动操作:
如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作。与拷贝操作不同,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作:
1 #include <iostream> 2 using namespace std; 3 4 //编译器会为X和hasX合成移动操作 5 6 struct X { 7 int i;//内置类型可以移动 8 std::string s;//string定义了自己的移动操作 9 }; 10 11 struct hasx { 12 X mem;//x有合成的移动操作 13 }; 14 15 int main(void){ 16 X x, x2 = std::move(x);//使用合成的移动构造函数 17 hasx hx, hx2 = std::move(hx);//使用合成的移动构造函数 18 19 return 0; 20 }
注意:只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非 static 数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
移动操作的删除函数:
与拷贝操作不同,移动操作永远不会隐式定义为删除函数。但是,如果我们显示地要求编译器生成 = default 的移动操作,且编译器不能移动所有非 static 成员,则编译器会将移动操作定义为删除函数。一般情况下,合成的移动操作定义为删除的函数要遵循与定义为删除的合成拷贝操作类似的原则:
与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值的情况类似
如果有类所有的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的
类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
类似拷贝赋值运算符,如果有类成员是 const 的或是引用的,则类的移动赋值运算符被定义为删除的
1 #include <iostream> 2 using namespace std; 3 4 class Y { 5 private: 6 int i; 7 std::string *s; 8 9 public: 10 Y(int a = 0, string ss = "") : i(a), s(new std::string(ss)) { 11 cout << "gou zao" << endl; 12 } 13 Y(const Y &it) : i(it.i), s(new string(*it.s)) { 14 cout << "kao bei gou zao" << endl; 15 } 16 Y(Y&&) = delete; 17 }; 18 19 //编译器将会给hasy合成一个删除的移动构造函数 20 class hasy { 21 public: 22 hasy() = default; 23 hasy(hasy&&) = default; 24 25 private: 26 Y mem; 27 }; 28 29 int main(void){ 30 hasy hy, hy2 = std::move(hy);//错误,hasy的移动构造函数是删除的 31 32 return 0; 33 }
注意:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作,否则,这些成员默认地被定义为删除的
移动右值,拷贝左值:
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数:
1 #include <iostream> 2 #include "StrVec.h" 3 using namespace std; 4 5 StrVec getVec(void) { 6 StrVec gg; 7 return gg; 8 } 9 10 int main(void) { 11 StrVec v1, v2; 12 v1 = v2;//v2是左值,使用拷贝赋值 13 14 v2 = getVec();//getVec()返回一个右值,使用移动赋值 15 16 return 0; 17 }
如果没有移动构造函数,右值也被拷贝:
1 #include <iostream> 2 using namespace std; 3 4 class Foo{ 5 public: 6 Foo() = default; 7 Foo(const Foo &it) : x(it.x) { 8 cout << "kao bei gou zao han shu" << endl; 9 } 10 11 private: 12 int x; 13 14 };//Foo定义了拷贝构造函数且未定义移动构造函数,因此其没有移动构造函数 15 16 int main(){ 17 Foo x; 18 Foo y(x);//拷贝构造函数,x是一个左值 19 Foo z(std::move(x));//拷贝构造函数,因为未定义移动构造函数 20 21 return 0; 22 }
注意:在对 z 进行初始化时,我们调用了 move(x),它返回一个绑定到 x 的 Foo&&。但 Foo 类没有移动构造函数,而且 Foo&& 转换为一个 const Foo& 是可行的,因此会匹配到 Foo 的拷贝构造函数
拷贝并交换赋值运算符和移动操作:
HasPtr.h:
1 #pragma once 2 3 #include <iostream> 4 5 class HasPtr{ 6 friend void swap(HasPtr&, HasPtr&); 7 8 public: 9 HasPtr(const std::string &s = std::string(), int a = 0) : ps(new std::string(s)), i(a) {} 10 HasPtr(const HasPtr&); 11 HasPtr(HasPtr&&) noexcept;//移动构造函数 12 HasPtr& operator=(HasPtr);//赋值运算符既是移动赋值运算符,也是拷贝赋值运算符 13 ~HasPtr(){ 14 delete ps; 15 } 16 17 std::ostream& print(std::ostream &os); 18 19 private: 20 std::string *ps; 21 int i; 22 };
HasPtr.cpp:
1 #include "HasPtr.h" 2 #include <iostream> 3 4 //拷贝构造函数 5 HasPtr::HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) { 6 std::cout << "kao bei gou zao han shu" << std::endl; 7 } 8 9 //移动构造函数 10 HasPtr::HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {//不会抛出异常,将其标记为noexcept 11 p.ps = nullptr;//确保移动后源对象是安全的 12 std::cout << "yi dong gou zao han shu" << std::endl; 13 } 14 15 //赋值运算符既是移动赋值运算符,也是拷贝赋值运算符 16 HasPtr& HasPtr::operator=(HasPtr rhs){//注意这里不能是引用 17 swap(*this, rhs);//交换后rhs指向本对象曾经使用的内存 18 std::cout << "fu zhi yun suan fu" << std::endl; 19 return *this;//作用域结束,rhs被销毁,从而delete了rhs中的指针 20 } 21 22 inline 23 void swap(HasPtr &lhs, HasPtr &rhs){ 24 std::swap(lhs.ps, rhs.ps); 25 std::swap(lhs.i, rhs.i); 26 } 27 28 std::ostream& HasPtr::print(std::ostream &os){ 29 os << i << " " << *ps << " " << ps; 30 return os; 31 }
ac31.cpp:
1 #include <iostream> 2 #include "HasPtr.h" 3 using namespace std; 4 5 int main(void){ 6 HasPtr hp2, hp; 7 hp = hp2;//hp2是一个左值,通过拷贝构造函数构造赋值运算符的形参 8 hp = std::move(hp2);//move返回右值,通过移动构造函数构造赋值运算符的形参 9 10 return 0; 11 }
注意:此类的运算符有一个非引用参数,这意味着此参数要进行拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能
移动迭代器:
c++11 标准库中定义了一种移动迭代器适配器,移动迭代器解引用生成一个右值引用。我们通过调用 make_move_iterator 函数将一个普通迭代器转换成一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。原迭代器的所有操作在移动迭代器中都照常工作:
1 void StrVec::reallocate(){ 2 auto newcapacity = size() ? 2 * size() : 1; 3 auto newdata = alloc.allocate(newcapacity);//分配新内存 4 5 //将旧的数据移动到新内存中 6 // auto dest = newdata;//指向新数组中下一个空闲位置 7 // auto elem = elements;//z指向旧数组中下一个位置 8 // for(size_t i = 0; i !=size(); ++i){ 9 // alloc.construct(dest++, std::move(*elem++));//移动而非构造一个新的string 10 // } 11 auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), newdata); 12 free();//释放旧内存 13 elements = newdata; 14 // first_free = dest; 15 first_free = last; 16 cap = elements + newcapacity; 17 }
注意:标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁原对象,因此只有确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能使用移动迭代器传递给算法
右值和左值引用成员函数:
通常,我们在一个对象上调用成员函数,而不管对象是一个左值还是一个右值:
1 #include <iostream> 2 using namespace std; 3 4 int main(void){ 5 string s1 = "a value", s2 = "another"; 6 auto n = (s1 + s2).find('a');//在一个string右值上调用find 7 8 s1 + s2 = "woe!";//给一个右值赋值,虽然这是毫无意义的,但是正确的 9 10 return 0; 11 }
引用限定符:
但是我们可能希望在自己的类中阻止给右值赋值。即强制左侧运算对象(this 指向的对象) 是一个左值:
指出 this 的左值 / 右值属性的方式与定义 const 成员函数相同——在参数列表后放置一个引用限定符:
1 #include <iostream> 2 using namespace std; 3 4 class Foo{ 5 public: 6 Foo& operator=(const Foo&) &;//只能向可修改的左值赋值, 7 // &和const一样用来修饰this指向的对象的 8 // Foo(); 9 // ~Foo(); 10 11 // Foo someMem() & const;//错误,const 限定符必须在前 12 Foo anotherMem() const &;//正确,const 限定符在前 13 14 Foo gg() const &&;//this 也可以指向一个右值 15 // Foo yy() && const;//指向右值时const同样不能在后面 16 }; 17 18 Foo& Foo::operator=(const Foo &rhs) & {//引用限定符和const一样必须同时出现在声明和定义中 19 //执行将rhs赋予本对象所需的工作 20 return *this; 21 } 22 23 Foo& retFoo(){//返回一个左值 24 return *(new Foo()); 25 } 26 27 Foo retVal(){//返回一个右值 28 return Foo(); 29 } 30 31 int main(void){ 32 Foo i, j;//i和j是左值 33 34 i = j;//正确,i是左值 35 36 retFoo() = j;//正确,retFoo() 返回一个左值 37 // retVal() = j;//错误,retVal() 返回一个右值 38 i = retVal();//正确,我们可以将一个右值作为赋值运算符右侧运算对象 39 40 return 0; 41 }
注意:引用限定符可以是 & 或 &&,分别指出 this 可以指向一个左值或右值。
类似 const 限定符,引用限定符只能用于非 static 成员函数且必须同时出现在函数的声明和定义中
一个函数可以同时用 const 和引用限定。但引用限定符必须跟随在 const 限定符之后
重载和引用函数:
1 #include <iostream> 2 #include <vector> 3 #include <algorithm> 4 using namespace std; 5 6 class Foo{ 7 public: 8 Foo sorted() &&;//可用于改变的右值 9 Foo sorted() const &;//可用于任何类型的Foo 10 // Foo sorted();//错误,当有两个以上同名且同参数列表的成员函数时, 11 // 要么全(指这些同名且同参数列表的成员函数)都加引用限定符(只要有就行, 12 // 不论右值还是左值引用),要么都不加引用限定符 13 Foo sorted(int);//正确,参数列表不同 14 15 private: 16 vector<int> data; 17 }; 18 19 //本对象为右值,因此可以原址排序 20 Foo Foo::sorted() && { 21 sort(data.begin(), data.end()); 22 return *this; 23 } 24 25 //本对象是const或是一个左值,哪种情况我们都不能原址排序 26 Foo Foo::sorted() const & { 27 Foo ret(*this);//拷贝一个副本 28 sort(ret.data.begin(), ret.data.end());//副本排序 29 return ret;//返回副本 30 } 31 32 Foo& retFoo(){//返回一个左值 33 return *(new Foo()); 34 } 35 36 Foo retVal(){//返回一个右值 37 return Foo(); 38 } 39 40 int main(void){ 41 retVal().sorted();//retVal() 返回一个右值,调用 Foo::sorted() && 42 retFoo().sorted();//retFoo() 返回一个左值,调用 Foo::sorted() const & 43 44 return 0; 45 }
注意:同名且具有相同参数列表的成员函数要么全部加引用限定符(只要有就行,不论是左值引用还是右值引用),要么全都不加引用限定符