移动

转载自:《C++primer》

重新分配内存时,使用移动而不是拷贝元素

编写reallocate成员函数之前,先思考下此函数需要做什么,该函数可以理解为vector<string>类型成员在当前容量使用光之后进行扩展存储的过程。

  • 为一个新的、更大的string数组分配内存
  • 在内存空间的前一部分构造对象,保存现有元素
  • 销毁原内存空间中的元素,并释放这块内存

可以看出为一个StrVec重新分配内存空间会引起从旧内存空间到新内存空间逐个拷贝string,当拷贝一个string时,新string和原string是相互独立的,改变原string不会影响到副本,整个过程中就会分配内存空间来拷贝string,一旦元素从旧空间拷贝到新空间,就会销毁原string释放其所占内存。

因此,拷贝这些string中的数据是多余的,在重新分配内存空间时,如果能避免分配和释放string的额外开销,StrVec的性能会好很多

移动构造函数和std::move

通过使用新标准引入的俩种机制,我们就可以避免string的拷贝,例如string等一些标准库类都定义了所谓的“移动构造函数”,移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象,而且我们知道标准库保证“移后源”string仍然保持一个有效的、可析构的状态

第二个机制是一个名为move的标准库函数,关于move我们需要了解俩个关键点,首先,当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数如果它漏掉了move的调用,将会使用string的拷贝构造函数,当我们使用move时,直接调用std::move而不是move。

了解这些知识就可以编写reallocate成员了:

construct的第二个参数(即确定使用哪个构造函数的参数)是move返回的值,调用move返回的结果会令construct使用string的移动构造函数,由于我们使用了移动构造函数,这些string管理的内存将不会别考别,相反,我们构造的每个string都会从elem指向的string那里接管内存的所有权。元素移动完毕后,调用free销毁旧元素并释放StrVec原来使用的内存,string成员不再管理它们曾经指向的内存,其数据的管理职责已经转移给了新StrVec内存中的元素了,我们不知道旧StrVec内存中的string包含什么值,但我们保证对它们执行string的析构函数是安全的。

对象移动

新标准的一个最主要的特性是可以移动而非拷贝对象的能力,在某些情况下,对象拷贝后就立即被销毁了,在这些情况下,移动而非拷贝对象会大幅度提升性能。使用移动而不是拷贝的另一个原因源于IO类unique_ptr这样的类,这些类都包含不能被共享的资源(如指针或IO缓冲),因此,这些类型的对象不能拷贝但可以移动。旧C++标准中,没有直接的方法移动对象,因此不得不使用拷贝,类似的,在旧版本的标准库中,容器所保存的类必须是可拷贝的,但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。标准库容器、string和shared_ptr类既支持移动也支持拷贝,IO类和unique_ptr类可以移动但不能拷贝。

移动构造函数

为了让自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符,这俩个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。移动构造函数的第一参数是该类类型的一个引用,不同于拷贝构造函数,这个引用参数在移动构造函数中是一个右值引用,与拷贝构造函数一样,任何额外的参数都必须有默认实参。除了完成资源移动,移动构造函数还必须确保移后源对象处于这样的状态----销毁它是无害的,特别是,一旦资源完成移动,源对象必须不再指向被移动的资源,这些资源所有权已经归属新创建的对象。最终移后源对象会被销毁。

noexcept:将移动构造函数和移动赋值运算符设置为noexcept是必须的它通知标准库我们的构造函数不会抛出任何异常,由于移动操作“窃取”资源,它通常不分配任何资源,因此通常不会抛出任何异常,但我们应该将此事通知标准库,否则它会认为移动我们的类对象时可能会抛出异常,并为处理这种可能性而做一些额外的工作,我们在一个函数参数列表后制定noexcept,在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept

class StrVec
{
public:
	StrVec(StrVec&&) noexcept; 
	int _i;
};
StrVec::StrVec(StrVec&& s) noexcept :_i(s._i) //成员初始化
{
	//函数体
}

移动赋值运算符

类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值,移动赋值运算符需要右侧运算对象的一个右值,我们进行检查的原因是此右值可能是move调用的返回结果,与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同的资源)

移后源对象必须可析构

从一个对象移动数据操作完成前不会销毁此对象,但有时在移动操作完成后,源对象会被销毁,因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态,但用户不能对其值进行任何假设。除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的,一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值

合成的移动操作

编译器在某些情况下回合成移动构造函数和移动赋值运算符,但例如一个类定义了自己的拷贝构造函数拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了,因此如果自己不实现的话,类就会使用对应的拷贝操作来代替移动操作

编译器主动合成情况:只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造或移动赋值运算符,编译器可以移动内置类型的成员,如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员。

与合成拷贝操作不同,移动操作永远不会隐式定义为删除的函数,但是如果我们显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数,除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则:

  • 移动构造函数被定义为删除的函数条件:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数,移动赋值情况类似。
  • 如果有类成员的移动构造函数或移动赋值被定义为删除的或不可访问的,则类的移动构造或移动赋值被定义为删除的
  • 如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的
  • 如果有类成员是const的或者是引用,则类的移动赋值被定义为删除的

移动操作和合成的拷贝控制成员间的相互作用关系:如果类定义一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符也会被定义为删除的。

移动右值,拷贝左值

如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它们时也是使用拷贝构造函数来替代。我们可以将一个n&&转换为一个const n&。

三/五法则

所有五个拷贝控制成员应该看作一个整体,一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作,拷贝构造函数定义了当用同类型的另一个对象初始化新对象时做什么,拷贝赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么,析构函数定义了此类型的对象销毁时做什么。定义移动构造函数移动赋值运算符就可以避免额外开销,我们将这些操作称为拷贝控制操作。

移动迭代器

StrVec的reallocate成员使用了一个for循环来调用construct从旧内存将元素拷贝到新内存,作为一种替换方法,如果我们能调用uninitialized_copy来构造新分配的内存,将比循环简单,但是它是对元素进行拷贝操作,标准库中并没有类似的函数将对象“移动”到未构造的内存中,新标准库中定义了一种移动迭代器适配器,一个的移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。

一般来说一个迭代器的解引用运算符返回一个指向元素的左值,与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作,由于移动迭代器支持正常的迭代器操作,我们可以将一对移动迭代器传递给算法,例如传递给uninitialized_copy。

值得注意的是,标准库不保证哪些算法适用移动迭代器,哪些不适用,由于移动一个对象可能销毁原对象,因此只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。

class A
{
public:
	A(int i):a(i)
	{
		//cout << "A()" << endl;
	}
	A(const A& another)
	{
		a = another.a;
		//cout << "A(const)" << endl;
	}
	~A()
	{
		cout << "~~" << endl;
	}
	A(A&& another)
	{
		a = another.a;
		cout << "&&" << endl;
	}
	int a;
};

int main()
{
	vector<A> v{ {A(1)},A(2),A(3) };
	//调用移动构造函数
	vector<A> v1(make_move_iterator(v.begin()), make_move_iterator(v.end()));

	for (auto record : v1)
		cout << record.a << endl;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值