13.6.1右值引用
| Int I = 42; Int &&rr = I;// 不能绑定到左值上 Int &&rr = I * 42;//正确 Const int &r3 = i*42;//const的引用可以绑定到右值上 Int &&r1 = 42; Int &&r2 = r1;//错误,r1是左值的 Int &&r2 =std::move(r1);//正确 |
1. 对象移动 1)移动而非拷贝对象可以大幅提升性能。 2)另一个原因是unique_ptr 与IO类这些类都包含不能被共享的资源。这些类型的对象不能拷贝但可以移动。 3)旧C++标准中没有直接的方法移动对象。对象本身要求分配内存空间进行不必要的拷贝,或者对象较大 那么拷贝代价非常高 2. 右值引用 1)右值引用就是必须绑定到右值的引用。通过&&而不是&来获得右值引用。 2)重要形状:只能绑定到一个将要销毁的对象。因此,可以将一个右值引用的资源“移动”到另一个对象中 3)左值表达式表示的是一个对象的身份,右值表达式表示的是对象的值 4)常规引用称为“左值引用”,不能绑定到要求转换的表达式、字面常量或是返回右值的表达式 5)右值引用不能直接绑定到一个左值上。 6)右值引用也是某个对象的另一个名字而已 3. 左值持久:右值短暂 右值只能绑定到临时对象 1) 所引用的对象将要被销毁 2) 该对象没有其它用户 右值引用的代码可以自由的接管所引用的对象的资源 Note:右值引用指向将要被销毁的对象。我们可以从绑定到右值引用的对象“窃取”状态 4. 变量是左值 1)变量表达式都是左值,一个右值引用是左值,所以不能将一个右值引用绑定到一个右值引用类型的变量上 2)因为变量是持久的,到离开作用域才被销毁。 Note:变量是左值,所以不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。 5. 标准库move函数 1)虽然不能直接,但可以显式地将一个左值转换为右值引用类型, utility头文件中move 2)Move意味承诺:除了对rr1(被移动对象)赋值和销毁它外,将不再使用它。调用move后,不能对移后源对象的值做任何假设使用 Note:可以销毁赋值一个移后源对象,但不能使用。 3) 直接使用std::move,不是用using声明。 |
13.6.2移动构造函数和移动赋值运算符
| Strvec::StrVec(StrVec &&s) noexcept :element(s.elements),first_free(s.first_free),cap(s.cap) { s.element=s.first_free=s.cap=nullptr } Strvec::StrVec(StrVec &&s) noexcept { If(this != &rhs){ … Rhs.element=nullptr; } Return *this; } Struct X{ Int I;//内置类型可以移动 Std::string s;//string定义了自己的移动操作 } Struct hasX{ X mem; } X x,x2=std::move(x); hasX hx,hx2 = std::move(hx); StrVec v1,v2; V1 = v2; StrVec getVec(istream&) V2 = getVec(cin);//getVec返回右值,使用移动赋值 Class Foo{ Public: Foo() = default; Foo(const Foo&); } Foo x; Foo z(std::move(x));//也是执行拷贝构造函数 HasPtr& operator=(HasPtr rhs) Hp = std::move(hp2);//拷贝并交换 Uninitialized_copy(make_move_iterator(begin()), make_move_iterator(edn()),first) |
1. 移动构造函数和移动赋值运算符 1)我们的类也同时支持移动和拷贝 2)是窃取资源 不是拷贝资源 3)写法形式与拷贝的差不多,不过是右引用,并且const不同 4)必须确保移后源对象处于销毁无害的。源对象不再指向被移动的资源 5)因为移动赋值后,若源对象被销毁,并且还指向被移动的资源,那么就会释放掉我们刚刚移动的内存 2. 移动操作、标准库容器和异常 1)因为是窃取,通常来说,移动操作不会抛出任何异常,应该通知标准库,不然标准库认为移动我们的类对象可能会抛出异常,并会作出一些额外的工作。一种是声明noexcept。 2)声明和定义的地方都需要noexcept,在参数列表之后 3)noexcept的作用 可以帮助我们深入理解标准库与我们自定义的类型交互的 移动操作可以发生异常 标准库容器能对异常发生时其自身的行为提供保障,如调用push_back时发生异常,vector自身不会改变 [1] 如果重新分配过程中使用了移动构造函数,移动了部分时发生了异常,让移动源元素已经改变,而新空间中未构造的元素可能尚不存在,vector将不能满足自身保持不变的要求 [2] 但是如果使用了拷贝构造,即使发生异常,释放新分配的内存并返回,vector原元素仍然存在。 结合1,2说明总结: vector如果知道元素类型的移动构造函数不会抛出异常,在重新分配内存的过程中,就使用移动构造函数,否则就使用拷贝构造函数 3. 移动赋值运算符 1)处理析构函数和移动构造函数相同的工作, 必须处理自赋值 2)直接先检查this与rhs是否同一个对象 4. 移后源对象必须可析构 1)从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会销毁。 所以必须移后源对象必须可析构 2)还需要对象仍然有效的,指:可以赋予新值或者可以安全地使用而不依赖其当前值。程序不应该依赖移后源对象中的数据 3)为了让移后源对象有效,不同类有不同的行为,像StrVec类是将成员为默认初始化时一样的状态,为nullptr Note:移后源对象必须保持有效的、可析构的状态,但用户不能对其值进行任何假设 5. 合成的移动操作 1)编译器根本不会为"某些类"合成移动操作,特别是一个类定义了自己的拷贝构造函数 拷贝赋值运算符或者析构函数。则不会为它合成移动,那么就是删除的移动操作。那么类会使用拷贝构造操作来代替移动操作,因为const int&可以绑定右值 2)只有当类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时 才会合成(内置类型可以移动,成员为标准库类对象且有对应的移动操作) 删除的移动操作 1)移动操作永远不会隐式定义为删除的函数。 2)显式的指定default 并且编译器不能移动所有成员,则定义为删除的 除了一个重要例外,合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类型的原则 1)类成员定义了拷贝构造未定义移动构造,或类成员为定义拷贝构造不能为其合成移动构造 2)类成员的移动构造被定义为删除或不可访问 3)类的析构函数被定义为删除或不可访问的 4)类成员是const的或引用,类的移动赋值运算符为删除,但移动拷贝构造可以 5) 最后一个 移动操作和合成的拷贝控制成员关系 一个类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成构造函数和拷贝赋值运算符会被定义为删除的 Note:定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的 6. 移动右值,拷贝左值 1)既有拷贝构造又有移动构造,编译器使用普通的函数匹配规则来确定 2)右值引用只接受实参是非static右值的情形 3)因为赋值运算符,右值可以用const 左引用引用,或者直接右引用引用,所以右值存在两个匹配 7. 没有移动构造函数,右值也被拷贝 1)若类有一个拷贝构造函数并没有移动构造函数。函数匹配规则保证该类型的对象会被拷贝,即使通过调用move来移动也是如此 2)可以将 int&& 转换为const int& 3)拷贝构造函数代替移动构造函数总是安全的。符合:拷贝给定对象,将源对象置于有效状态。 Noe:移动赋值运算符类似 8. 拷贝并交换 赋值运算符和移动操作 1)定义拷贝并交换赋值运算符,添加一个移动构造函数,则也会获得一个移动赋值运算符 2)结合临时对象和swap创建一个即是移动赋值运算符 也可以是拷贝赋值运算符 3)如果传的拷贝并交换形参是右值,则调用移动构造函数,执行移动赋值运算符操作 如果传的拷贝并交换形参是左值,则调用拷贝构造函数,执行拷贝赋值运算符操作 4)Swap交换两个运算对象的状态。Rhs离开作用域时,这个string被销毁 建议:更新三/五法则 五个拷贝控制成员应该看做一个整体,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。通常一个类有一个资源成员那么需要拷贝成员,但是拷贝成员额外开销,所以定义移动的就可以避免 9. Message类的移动操作 1)就是一个概念,使用移动操作是将右侧的移动到左侧后,右侧运算对象称为移后源对象就不会再在原先的floder集合中了 2)并且减少不必要的消耗 10. 移动迭代器 1)for循环construct很慢,用uninitialized_copy来简单,需使用移动迭代器适配器 2)一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器 解引用移动迭代器生成一个右值引用 3)make_move_iterator移动迭代器支持正常迭代器的操作,可以传给算法 4)由于移动一个对象可能销毁掉原对象,只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法 建议:不要随意使用移动操作 note:确信move是安全的再使用。不然莫名其妙,难以查找的错误 |
13.6.3右值引用和成员函数
| Void push_back(const x&); VOID push_back(X&&); Alloc.construct(first_Free++,std::move(s)); (s1 + s2).find(‘a’);//用右值调用函数 S1+s2 = “wow!”;对右值进行赋值 Foo & Foo::operator=(const Foo&)& Foo someMem() const &; Foo someMem() &&; 引用限定符 |
1. 右值引用和成员函数 1)可以为成员函数提供拷贝和移动版本,能从中受益。参数是与拷贝/移动赋值运算符相同的参数模式 2)一个是const左值引用 const X& 一个是指向非const的右值引用 X&&右值和左值引用成员函数 3)一般来说不需要定义相对的X&和const X&& const X&&:我们希望从实参“窃取”数据,通常是右值引用,不是const右值引用 X&: 从一个对象进行拷贝的操作通常不改变该对象 4)Alloc.construct(first_Free++,std::move(s));,move返回右值引用,所以会使用移动构造函数来构造新元素 5)实参类型决定新元素是拷贝还是移动到容器中。 2. 右值和左值引用成员函数 1)右值和左值 对象都可以调用成员函数 2)不希望右值调用或者被赋值,我们可以阻止,强制左侧运算对象(this指向的对象)是一个左值才可以调用成员函数 3)定义方式:在成员函数参数列表后放置一个引用限定符,与定义const相同 4)只允许可修改的左值赋值需要在拷贝赋值运算符 添加& 限定,声明与定义地方都需要 5)与const组合使用需要放在const后 自己总结: 1)若都对成员函数没有限定符,则this可以表示左值或右值都可以,即可以为左值或右值赋值 2)若只对左值限定(拷贝赋值运算符),则只能对左值赋值,右值不可以 3)若只对右值限定 则相反 4)若对左值和右值都限定则都可以 3. 重载和引用函数 1)引用限定符也可以区分重载版本,可以通过与const的结合重载 2)有四种const & 、&&、&、const&&限定符但只需要 Const &:& ,const &&,const &都会转换为这个 &&,:只有&&精确匹配 3)右值版本的函数可以直接使用,因为没有其它用户。Const&版本不能改变对象,所以需要拷贝后操作。 4)定义const成员函数时可以有两个版本,但引用限定不行,若定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加 代码: Foo136 f1; // 调用 & f1.someMem(); const Foo136 f2; std::move(f2).someMem();// 调用const&& void someMem() &&; void someMem() const &; void someMem() &; void someMem() const &&; void Foo136::someMem() && { cout << "Foo136 Foo136::someMem() &&" << endl; } void Foo136::someMem() const & { cout << "Foo136 Foo136::someMem() const &" << endl; } void Foo136::someMem() & { cout << "Foo136 Foo136::someMem() &" << endl; } void Foo136::someMem() const && { cout << "Foo136 Foo136::someMem() const &&" << endl; } |