对象移动
Posted summer-8918
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对象移动相关的知识,希望对你有一定的参考价值。
在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类包含不能被共享的资源(如指针或IO缓冲),因此,这些类型的对象不能拷贝,但是可以移动。
标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。
右值引用
右值引用是必须绑定到右值的引用,通过&&来获得右值引用。右值引用只能绑定到一个将要销毁的对象。因此可以自由地将一个右值引用的资源“移动”到另一个对象中。
一个左值表示一个对象的身份,一个右值表示对象的值。
左值引用:不能将其绑定到要求转换的表达式、字面值常量或是返回右值的表达式。
右值引用:与左值引用相反,可以绑定到这类表达式上。
返回左值引用的函数,连同赋值、下标、借用用和前置递增/递减运算符,都是返回左值的表达式的例子。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值绑定到这类表达式上。
左值持久;右值短暂。
右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态
变量是左值
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
标准库move函数
可以显式地将一个左值转换为对应的右值引用类型。通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。move定义在utility头文件中。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
移动构造函数和移动赋值运算符
StrVec::StrVec(StrVec &&s) noexcept //移动操作不应该抛出任何异常
//成员初始化器接管s中的资源
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
//另s进入这样一种状态——对其进行析构函数是安全的
s.elements=s.first_free=s.cap=nullptr;
}
第一个参数是该类类型的右值引用。除了完成资源移动,移动构造函数还必须保证移后源对象处于这样一个状态——销毁它是无害的。
noexcept 通知标准库我们的构造函数不抛出任何异常。
由于移动操作“窃取”资源,它通常不分配任何资源,因此移动操作通常不会抛出任何异常。noexcept是我们承诺一个函数不抛出异常的一种方法。
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept.
移动赋值运算符
如果右侧和左侧运算对象指向相同的对象,不需要做任何事情;否则,释放左侧对象所使用的内存,并接管给定对象的内存。
不能使用右侧运算对象之前就释放左侧运算对象资源(可能是相同的资源)
移后源对象必须可析构
在移动操作之后,移后源对象必须保持有效的、可析构的状态,但用户不能对其值进行任何假设。
合成的移动操作
如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器不会为它合成移动构造函数和移动赋值运算符。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型成员。
与拷贝控制不同,移动操作永远不会隐式定义为删除的函数。
如果我们显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝控制操作。
移动右值,拷贝左值
但如果没有移动构造函数,右值也被拷贝
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。拷贝赋值函数和移动赋值函数类似。
赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
更新三/五法则
所有五个拷贝控制成员应该被看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作。这些类通常拥有一个资源,而拷贝成员必须拷贝此资源。一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类可以避免此问题。
移动迭代器
一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。移动迭代器的解引用运算符生成一个右值引用。
调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。
不要随意使用移动操作
由于一个移后源对象具有不确定的状态,对其调用std::move是危险的。当我们调用move时,必须绝对确认移后源对象没有其他用户。
在移动构造函数和移动赋值运算符这些类实现代码之外的地方,只有当你确信需要进行移动操作且移动操作时安全的,才使用std::move。
右值引用和成员函数
如果一个成员函数同时提供拷贝和移动版本,它也能从中受益
这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式——一个版本接受一个指向const的左值引用,第一个版本接受一个指向非const的右值引用。
class StrVec{ public: void push_back(const std::string&); //拷贝元素 void push_back(std::string&&); //移动元素 } void StrVec::push_back(const string& s) { chk_n_alloc(); alloc.construct(free_first++,s); } void StrVec::push_back(const string&& s) { chk_n_alloc(); alloc.construct(free_first++,std::move(s)); }
右值和左值引用成员函数
指出this的左值/右值属性与const成员函数相同。即,在参数列表后面放置一个引用限定符
class Foo{ public: Foo &operator=(const Foo&) &; //只能向可修改的左值赋值 }; Foo &Foo::operator=(const Foo &rhs) & { //执行将rhs赋予本对象所需的工作 return *this; }
引用限定符可以是&或&&,分别指出this可以指向一个左值或一个右值。
一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在const限定符之后。
区分移动和拷贝的重载函数通常由一个版本接受一个const T&,而另一个版本接受一个T&&。
以上是关于对象移动的主要内容,如果未能解决你的问题,请参考以下文章