C++Primer学习(13.6 对象移动)

news2025/3/31 23:23:07

13.6 对象移动
新标准的一个最主要的特性是可以移动而非拷贝对象的能力。如我们在13.1.1节(第440页)中所见,很多情况下都会发生对象拷贝。在其中某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
如我们已经看到的,我们的strvec类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。使用移动而不是拷贝的另一个原因源于I0类或unique_ptr这样的类。这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动。
在旧C++标准中,没有直接的方法移动对象。因此,即使不必拷贝对象的情况下,我们也不得不拷贝。如果对象较大,或者是对象本身要求分配内存空间(如string),进行不必要的拷贝代价非常高。类似的,在旧版本的标准库中,容器中所保存的类必须是可拷贝的。但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。
Note:标准库容器、string和shared_ptr类既支持移动也支持拷贝。I0类和unique_ptr类可以移动但不能拷贝。
13.6.1 右值引用
为了支持移动操作,新标准引入了一种新的引用类型–右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质–只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
回忆一下,左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用(lvalue reference)),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

int i = 42;
int &r =i;//正确:r引用i
int &&rr =i;//错误:不能将一个右值引用绑定到一个左值上
int &r2=i* 42;//错误:i*42是一个右值
const int &r3=i*42;//正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2=i* 42;//正确:将rr2绑定到乘法结果上

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
左值持久;右值短暂
考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,我们得知
(1)所引用的对象将要被销毁
(2)该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
Note:右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。
变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上,这有些令人惊讶:

int &&rrl=42;//正确:字面常量是右值
int &&rr2=rr1;//错误:表达式rr1是左值!

其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。
变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
标准库 move 函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。move函数使用了我们将在 16.2.6节(第610页)中描述的机制来返回给定对象的右值引用。

int &&rr3= std::move(rrl);//ok

move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move 就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用 move 之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
如前所述,与大多数标准库名字的使用不同,对move(参见13.5节,第469页)我们不提供using声明(参见3.1节,第74页)。我们直接调用std::move 而不是move,其原因将在18.2.3节(第707页)中解释。
WARNING:使用move的代码应该使用std::move而不是move。这样做可以避免潜在的名字冲突。
13.6.2移动构造函数和移动赋值运算符
类似string类(及其他标准库类),如果我们自己的类也同时支持移动和拷贝,那么也能从中受益。为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态–销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源–这些资源的所有权已经归属新创建的对象。
作为一个例子,我们为strVec类定义移动构造函数,实现从一个strVec 到另一个strVec的元素移动而非拷贝:

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(它通知标准库我们的构造函数不抛出任何异常),但让我们先分析一下此构造函数完成什么工作。
与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管给定的strVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行析构函数。strVec的析构函数在first_free 上调用 deallocate。如果我们忘记了改变s.first_free,则销毁移后源对象就会释放掉我们刚刚移动的内存。
移动操作、标准库容器和异常
由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
一种通知标准库的方法是在我们的构造函数中指明noexcept。noexcept是新标准引入的,我们将在18.1.4节(第690页)中讨论更多细节。目前重要的是要知道,noexcept是我们承诺一个函数不抛出异常的一种方法。我们在一个函数的参数列表后指定noexcept。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间:

class StrVec
{
	public:
	StrVec(StrVec&&)noexcept;//移动构造函数
	//其他成员的定义,如前
};
StrVec::StrVec(StrVec&&s)noexcept:/*成员初始化器*/{/*构造函数体 */}

我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
搞清楚为什么需要noexcept能帮助我们深入理解标准库是如何与我们自定义的类型交互的。我们需要指出一个移动操作不抛出异常,这是因为两个相互关联的事实:首先,虽然移动操作通常不抛出异常,但抛出异常也是允许的;其次,标准库容器能对异常发生时其自身的行为提供保障。例如,vector保证,如果我们调用push_back时发生异常vector自身不会发生改变。
现在让我们思考push_back内部发生了什么。类似对应的strVec操作(参见13.5节,第466页),对一个vector调用push_back可能要求为vector 重新分配内存空间。当重新分配vector的内存时,vector将元素从旧空间移动到新内存中,就像我们在reallocate中所做的那样。如我们刚刚看到的那样,移动一个对象通常会改变它的值。如果重新分配过程使用了移动构造函数,且在移动了部分而不是全部元素后抛出了一个异常,就会产生问题。旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。在此情况下vector将不能满足自身保持不变的要求。
另一方面,如果vector使用了拷贝构造函数且发生了异常,它可以很容易地满足要求。在此情况下,当在新内存中构造元素时,旧元素保持不变。如果此时发生了异常,vector可以释放新分配的(但还未成功构造的)内存并返回。vector原有的元素仍然存在。
为了避免这种潜在问题,除非vector知道元素类型的移动构造函数不会抛出异常否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在 vector 重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。
移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:

StrVec &StrVec::operator=(StrVec &&rhs)noexcept
{
	//直接检测自赋值
	if (this != &rhs)
	{
		free();//释放已有元素
		elements=rhs.elements;//从rhs接管资源
		first free =rhs.first_free;
		cap =rhs.cap;
		//将rhs置于可析构状态
		rhs.elements=rhs.first free =rhs.cap =nullptr;
	}
	return *this;
}

在此例中,我们直接检查this指针与rhs的地址是否相同。如果相同,右侧和左侧运算对象指向相同的对象,我们不需要做任何事情。否则,我们释放左侧运算对象所使用的内存,并接管给定对象的内存。与移动构造函数一样,我们将rhs中的指针置为nullptr.我们费心地去检查自赋值情况看起来有些奇怪。毕竟,移动赋值运算符需要右侧运算对象的一个右值。我们进行检查的原因是此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同的资源)。
移后源对象必须可析构
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。我们的StrVec的移动操作满足这一要求,这是通过将移后源对象的指针成员置为nullptr来实现的。
除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效的。一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据。
例如,当我们从一个标准库string或容器对象移动数据时,我们知道移后源对象仍然保持有效。因此,我们可以对它执行诸如empty或size这些操作。但是,我们不知道将会得到什么结果。我们可能期望一个移后源对象是空的,但这并没有保证。
我们的 strVec类的移动操作将移后源对象置于与默认初始化的对象相同的状态。因此,我们可以继续对移后源对象执行所有的strVec操作,与任何其他默认初始化的对象一样。而其他内部结构更为复杂的类,可能表现出完全不同的行为。
WARNING:在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
合成的移动操作
与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符。但是,合成移动操作的条件与合成拷贝操作的条件大不相同。回忆一下,如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作。拷贝操作要么被定义为逐成员拷贝,要么被定义为对象赋值,要么被定义为删除的函数。
与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。因此,某些类就没有移动构造函数或移动赋值运算符。如我们将在第477页所见,如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员:

//编译器会为X和hasx合成移动操作
struct X
{
	int i;//内置类型可以移动
	std::string s;// string定义了自己的移动操作
};
struct hasX
{
	Xmem;//X有合成的移动操作
};
X x,x2 = std::move(x);//使用合成的移动构造函数
hasX hx,hx2= std::move(hx);//使用合成的移动构造函数

Note:只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则:
(1)与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
(2)如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的则类的移动构造函数或移动赋值运算符被定义为删除的。
(3)类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
(4)类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
例如,假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数:

//假定丫是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数
struct hasY 
{
	hasY()= default;
	hasY(hasY&&)=default;
	Y mem;//hasY将有一个删除的移动构造函数
};
hasY hy,hy2=std::move(hy);//错误:移动构造函数是删除的

编译器可以拷贝类型为Y的对象,但不能移动它们。类hasy显式地要求一个移动构造函数,但编译器无法为其生成。因此,hasY会有一个删除的移动构造函数。如果hasy忽略了移动构造函数的声明,则编译器根本不能为它合成一个。如果移动操作可能被定义为删除的函数,编译器就不会合成它们。
移动操作和合成的拷贝控制成员间还有最后一个相互作用关系:一个类是否定义了自己的移动操作对拷贝操作如何合成有影响。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。
移动右值,拷贝左值……
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。例如,在我们的strVec类中,拷贝构造函数接受一个const strVec的引用。因此,它可以用于任何可以转换为strVec的类型。而移动构造函数接受一个strVec&&,因此只能用于实参是(非static)右值的情形:

StrVec vl,v2;
v1= v2;//v2是左值;使用拷贝赋值
StrVec getVec(istream &);// getVec返回一个右值
v2 = getVec(cin);//getVec(cin)是一个右值;使用移动赋值

在第一个赋值中,我们将v2传递给赋值运算符。v2的类型是strVec,表达式v2是一个左值。因此移动版本的赋值运算符是不可行的,因为我们不能隐式地将一个右值引用绑定到一个左值。因此,这个赋值语句使用拷贝赋值运算符。
在第二个赋值中,我们赋予v2的是getVec 调用的结果。此表达式是一个右值。在此情况下,两个赋值运算符都是可行的–将getVec的结果绑定到两个运算符的参数都是允许的。调用拷贝赋值运算符需要进行一次到const的转换,而strVec&&则是精确匹配。因此,第二个赋值会使用移动赋值运算符。
但如果没有移动构造函数,右值也被拷贝
如果一个类有一个拷贝构造函数但未定义移动构造函数,会发生什么呢?在此情况编译器不会合成移动构造函数,这意味着此类将有拷贝构造函数但不会有移动构造函数。如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move 来移动它们时也是如此:

class Foo
{
	public:
	Foo()= default;
	Foo(const Foo&);//拷贝构造函数
	//其他成员定义,但Foo未定义移动构造函数
};
Foo x;
Foo y(x);//拷贝构造函数;x是一个左值
Foo z(std::move(x));//拷贝构造函数,因为未定义移动构造函数

在对z进行初始化时,我们调用了move(x),它返回一个绑定到x的Foo&&。Foo的拷贝构造函数是可行的,因为我们可以将一个Foo&&转换为一个const Foo&。因此,z的初始化将使用Foo的拷贝构造函数。
值得注意的是,用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符的情况类似)。一般情况下,拷贝构造函数满足对应的移动构造函数的要求:它会拷贝给定对象,并将原对象置于有效状态。实际上,拷贝构造函数甚至都不会改变原对象的值。
如果一个类有一个可用的拷贝构造函数而没有移动构造函数,则其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动赋值运算符的情况类似。
拷贝并交换赋值运算符和移动操作
我们的 HasPtr版本定义了一个拷贝并交换赋值运算符,它是函数匹配和移动操作间相互关系的一个很好的示例。如果我们为此类添加一个移动构造函数,它实际上也会获得一个移动赋值运算符:

Class HasPtr
{
public:
	//添加的移动构造函数
	HasPtr(HasPtr &&p)noexcept:ps(p.ps)i(p.i){p.ps=0;}
	//赋值运算符既是移动赋值运算符,也是拷贝赋值运算符
	HasPtr& operator=(HasPtrrhs)
	{swap(*this,rhs);return *this;}
	//其他成员的定义,同13.2.1节(第453页)
}

在这个版本中,我们为类添加了一个移动构造函数,它接管了给定实参的值。构造函数体将给定的HasPtr的指针置为0,从而确保销毁移后源对象是安全的。此函数不会抛出异常,因此我们将其标记为noexcept。
现在让我们观察赋值运算符。此运算符有一个非引用参数,这意味着此参数要进行拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数–左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能。
例如,假定hp和hp2都是HasPtr对象:

hp=hp2;//hp2是一个左值;hp2通过拷贝构造函数来拷贝
hp=std::move(hp2);//移动构造函数移动hp2

在第一个赋值中,右侧运算对象是一个左值,因此移动构造函数是不可行的。rhs将使用拷贝构造函数来初始化。拷贝构造函数将分配一个新string,并拷贝hp2指向的string。
在第二个赋值中,我们调用std::move 将一个右值引用绑定到hp2上。在此情况下,拷贝构造函数和移动构造函数都是可行的。但是,由于实参是一个右值引用,移动构造函数是精确匹配的。移动构造函数从hp2拷贝指针,而不会分配任何内存。
不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体都swap两个运算对象的状态。交换 HasPtr会交换两个对象的指针(及int)成员。在swap之后,rhs中的指针将指向原来左侧运算对象所拥有的string。当rhs离开其作用域时,这个string 将被销毁。
建议:更新三/五法则
所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作。这些类通常拥有一个资源,而拷贝成员必须拷贝此资源。一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。
Message类的移动操作
定义了自己的拷贝构造函数和拷贝赋值运算符的类通常也会从移动操作受益。例如,我们的Message和Folder类就应该定义移动操作。通过定义移动操作,Message类可以使用string和set的移动操作来避免拷贝contents和folders 成员的额外开销。
但是,除了移动 folders 成员,我们还必须更新每个指向原Message的 Folder。
我们必须删除指向旧Message的指针,并添加一个指向新Message的指针。
移动构造函数和移动赋值运算符都需要更新Folder指针,因此我们首先定义一个操作来完成这一共同的工作:

//从本Message移动Folder指针
void Message::move Folders(Message *m)
{
	folders=std::move(m->folders);//使用set的移动赋值运算符
	for(autof:folders){//对每个Folder
		f->remMsg(m);//从Folder中删除旧Message
		f->addMsg(this);//将本Message添加到Folder中
	}
	m->folders.clear();//确保销毁m是无害的
}

此函数首先移动 folders集合。通过调用move,我们使用了set的移动赋值运算符而不是它的拷贝赋值运算符。如果我们忽略了move调用,代码仍能正常工作,但带来了不必要的拷贝。函数然后遍历所有Folder,从其中删除指向原Message的指针并添加指向新 Message 的指针。
值得注意的是,向set插入一个元素可能会抛出一个异常–向容器添加元素的操作要求分配内存,意味着可能会抛出一个bad_alloc异常。因此,与我们的HasPtr和strVec类的移动操作不同,Message的移动构造函数和移动赋值运算符可能会抛出异常。因此我们未将它们标记为noexcep。
函数最后对m.folders调用clear。在执行了move 之后,我们知道m.folders是有效的,但不知道它包含什么内容。由于Message的析构函数遍历folders,我们希望能确定set是空的。
Message的移动构造函数调用move来移动contents,并默认初始化自己的folders 成员:

Message::Message(Message &m): contents(std::move(m.contents))
{
	move_Folders(&m);//移动folders并更新Folder指针
}

在构造函数体中,我们调用了move_Folders来删除指向m的指针并插入指向本Message 的指针。
移动赋值运算符直接检查自赋值情况:

Message& Message::operator=(Message &&rhs)
{
	if (this != &rhs){
		remove_from_Folders();
		//直接检查自赋值情况
		contents=std::move(rhs.contents);//移动赋值运算符
		move_Folders(&rhs);//重置Folders指向本Message
	}
return *this;
}

与任何赋值运算符一样,移动赋值运算符必须销毁左侧运算对象的旧状态。在本例中,销毁左侧运算对象要求我们从现有folders中删除指向本essage 的指针,我们调用remove_from_Folders 来完成这一工作。完成删除工作后,我们调用move从rhs将contents移动到this 对象。剩下的就是调用move_Messages 来更新 Folder 指针了。
移动迭代器
StrVec的reallocate成员使用了一个for 循环来调用construct从旧内存将元素拷贝到新内存中。作为一种替换方法,如果我们能调用uninitialized_copy来构造新分配的内存,将比循环更为简单。但是,uninitialized _copy恰如其名:它对元素进行拷贝操作。标准库中并没有类似的函数将对象“移动”到未构造的内存中。
新标准库中定义了一种移动迭代器(move iterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。
我们通过调用标准库的make_move_iterator 数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。
原迭代器的所有其他操作在移动迭代器中都照常工作。由于移动迭代器支持正常的迭代器操作,我们可以将一对移动迭代器传递给算法。特别是,可以将移动迭代器传递给uninitialized_copy:

void StrVec::reallocate()
{
	//分配大小两倍于当前规模的内存空间
	auto newcapacity=size()?2*size():1;
	auto first =alloc.allocate(newcapacity);
	//移动元素
	auto last = uninitialized_copy(make_move_iterator (begin()),
	                                               make_move_iterator(end()),
	                                                first);
	free();//释放旧空间
	elements =first;//更新指针
	first_free =last;
	cap =elements+newcapacity;
}

uninitialized_copy对输入序列中的每个元素调用construct 来将元素“拷贝”到目的位置。此算法使用迭代器的解引用运算符从输入序列中提取元素。由于我们传递给它的是移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着construct将使用移动构造函数来构造元素。
值得注意的是,标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。
建议:不要随意使用移动操作
由于一个移后源对象具有不确定的状态,对其调用std::move是危险的。当我们调用move时,必须绝对确认移后源对象没有其他用户。
通过在类代码中小心地使用move,可以大幅度提升性能。而如果随意在普通用户代码(与类实现代码相对)中使用移动操作,很可能导致莫名其妙的、难以查找的错误而难以提升应用程序性能。
Best在移动构造函数和移动赋值运算符这些类实现代码之外的地方,只有当你确信Practices需要进行移动操作且移动操作是安全的,才可以使用std::move。
13.6.3 右值引用和成员函数
除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式–一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。
例如,定义了push_back的标准库容器提供两个版本:一个版本有一个右值引用参数,而另一个版本有一个const左值引用。假定x是元素类型,那么这些容器就会定义以下两个push_back 版本:

Xvoid push_back(const X&);//拷贝:绑定到任意类型的
void push_back(X&&);//移动:只能绑定到类型X的可修改的右值

我们可以将能转换为类型x的任何对象传递给第一个版本的push_back。此版本从其参数拷贝数据。对于第二个版本,我们只可以传递给它非const的右值。此版本对于非const的右值是精确匹配(也是更好的匹配)的,因此当我们传递一个可修改的右值时,编译器会选择运行这个版本。此版本会从其参数窃取数据。
一般来说,我们不需要为函数操作定义接受一个constx&&或是一个(普通的)X&参数的版本。当我们希望从实参“窃取”数据时,通常传递一个右值引用。为了达到这目的,实参不能是const的。类似的,从一个对象进行拷贝的操作不应该改变该对象因此,通常不需要定义一个接受一个(普通的)x&参数的版本。
区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个 T&&。
作为一个更具体的例子,我们将为strvec类定义另一个版本的push_back:

class StrVec 
{
public:
	void push_back(const std::string&);//拷贝元素
	void push_back(std::string&&);// 移动元素
	//其他成员的定义,如前
};
//与13.5节(第466页)中的原版本相同
void StrVec::push_back(const string& s)
{
	chk_n_alloc();//确保有空间容纳新元素
	//在first_free指向的元素中构造s的一个副本
	alloc.construct(first_free++,s);
}
void StrVec::push_back(string &&s)
{
	chk_n_alloc();//如果需要的话为StrVec 重新分配内存
	alloc.construct(first_free++,std::move(s));
}

这两个成员几乎是相同的。差别在于右值引用版本调用move来将其参数传递给construct。如前所述,construct函数使用其第二个和随后的实参的类型来确定使用哪个构造函数。由于move返回一个右值引用,传递给construct的实参类型是string&&。因此,会使用string的移动构造函数来构造新元素。
当我们调用push_back时,实参类型决定了新元素是拷贝还是移动到容器中:

StrVec vec;//空StrVec
string s="some string or another";
vec.push_back(s);//调用push_back(const string&)
vec.push_back("done");//调用push_back(string&&)

这些调用的差别在于实参是一个左值还是一个右值(从"done"创建的临时 string),具体调用哪个版本据此来决定
右值和左值引用成员函数
通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值。例如:

string s1="avalue",s2 ="another";
auto n=(s1+s2).find('a');

此例中,我们在一个string右值上调用find成员,该string右值是通过连接两个string而得到的。有时,右值的使用方式可能令人惊讶:
sl+s2 =“wow!”;
此处我们对两个string的连接结果–一个右值,进行了赋值。
在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是,我们可能希望在自己的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即,this指向的对象)是一个左值。
我们指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符(reference qualifier):

class Foo 
{
public :
	Foo &operator=(const Foo&)&;//只能向可修改的左值赋值
	// Foo 的其他参数
};
Foo &Foo::operator=(const Foo &rhs)&
{
	//执行将 rhs 赋予本对象所需的工作
	return *this;
}

引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const 限定符引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值:

Foo &retFoo();//返回一个引用;retFoo 调用是一个左值
Foo retVal();//返回一个值;retVal调用是一个右值
Foo i,j;//i和j是左值
i =j;//正确:i是左值
retFoo()=j;//正确:retFoo()返回一个左值
retVal()=j;//错误:retVal()返回一个右值
i = retVal();//正确:我们可以将一个右值作为赋值操作的右侧运算对象

一个函数可以同时用const和引用限定。在此情况下,引用限定符必须跟随在 const限定符之后:

class Foo 
{
public:
	Foo someMem() & const;//错误:const限定符必须在前
	//正确:const限定符在前Foo  anotherMem() const&:
};

重载和引用函数
就像一个成员函数可以根据是否有const来区分其重载版本一样,引用限定符也可以区分重载版本。而且,我们可以综合引用限定符和const来区分一个成员函数的重载版本。例如,我们将为Foo定义一个名为data的vector 成员和一个名为sorted 的成员函数,sorted返回一个Foo对象的副本,其中 vector已被排序:

class Foo
{
public:
	Foo sorted()&&;//可用于可改变的右值
	Foo sorted()const&;//可用于任何类型的Foo
	// Foo 的其他成员的定义
private :
	vector<int> data;
};
//本对象为右值,因此可以原址排序
Foo Foo::sorted()&&
{
	sort(data.begin(),data.end());
	return *this;
}
//本对象是 const或是一个左值,哪种情况我们都不能对其进行原址排序
Foo Foo::sorted()const&
{
	Foo ret(*this);//拷贝一个副本
	sort(ret.data.begin(),ret.data.end());//排序副本
	return ret;//返回副本
}

当我们对一个右值执行sorted时,它可以安全地直接对data成员进行排序。对象是一个右值,意味着没有其他用户,因此我们可以改变对象。当对一个const右值或一个左值执行 sorted 时,我们不能改变对象,因此就需要在排序前拷贝data。
编译器会根据调用sorted的对象的左值/右值属性来确定使用哪个sorted 版本:
retVal().sorted();//retVal()是一个右值,调用Foo::sorted()&&
retFoo().sorted();//retFoo()是一个左值,调用Foo::sorted()const &
当我们定义const成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定而另一个没有。引用限定的函数则不一样。如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数,就必须对所有函数都加上引用限定符,或者所有都不加:

Class Foo
{
public:
	Foo sorted()&&;
	Foo sorted()const;//错误:必须加上引用限定符
	//Comp是函数类型的类型别名(参见6.7节,第222页)
	//此函数类型可以用来比较int值
	using Comp=bool(const int&const int&);
	Foo sorted(Comp*);//正确:不同的参数列表
	Foo sorted(Comp*)const;//正确:两个版本都没有引用限定符
}

本例中声明了一个没有参数的const版本的sorted,此声明是错误的。因为Foo类中还有一个无参的sorted版本,它有一个引用限定符,因此const版本也必须有引用限定符。另一方面,接受一个比较操作指针的sorted版本是没问题的,因为两个函数都没
有引用限定符。
Note如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2323826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

内核、进程和线程---操作系统

操作系统 操作系统位于用户程序和硬件之间&#xff0c;通过系统调用提供接口可以让应用程序去使用硬件&#xff0c;但是硬件资源的管理和安全控制由操作系统负责。 用户空间和内存空间 在计算机系统中&#xff0c;内存可以分为两大区域&#xff1a;内核空间&#xff08;Ker…

如何在 Postman 中上传图片并在请求中正确引用?

Postman 是一款常用的 API 测试工具&#xff0c;它不仅可以测试 API 的请求和响应&#xff0c;还支持多种数据格式包括图片。如何在 Postman 中传输图片&#xff1f; Postman 如何上传图片并在请求中使用教程

安全+低碳+高效:Acrel-3000助力企业打造未来型电能管理体系-安科瑞黄安南

一 背景 电能因为方便传输、易于转换、便于控制等特性&#xff0c;成为广大企事业单位生产、办公最主要的能量来源。双碳背景下&#xff0c;由于电能清洁、高效、零排放的特点&#xff0c;能源消费侧将逐步以电代煤、以电代油、以电代气&#xff0c;形成以电为中心的能源消费体…

专注自习室:番茄工作法实践

专注自习室&#xff1a;番茄工作法实践 我需要一个任务管理工具&#xff0c;但在网上找了很多都找不到合适的工具。市面上的大多数产品过于强调任务完成性&#xff0c;给我带来了很强的心理压力&#xff0c;这种压力最终反而降低了我的工作效率。于是我决定自己动手&#xff0…

LeetCode算法题(Go语言实现)_16

题目 给定一个二进制数组 nums 和一个整数 k&#xff0c;假设最多可以翻转 k 个 0 &#xff0c;则返回执行操作后 数组中连续 1 的最大个数 。 一、代码实现 func longestOnes(nums []int, k int) int {left, zeroCnt, maxLen : 0, 0, 0for right : 0; right < len(nums); …

CORDIC算法:三角函数的硬件加速革命——从数学原理到FPGA实现的超高效计算方案

计算机该如何求解三角函数&#xff1f;或许你的第一印象是采用泰勒展开&#xff0c;或者采用多项式进行逼近。对于前者&#xff0c;来回的迭代计算开销成本很大&#xff1b;对于后者&#xff0c;多项式式逼近在较窄的范围內比较接近&#xff0c;超过一定范围后&#xff0c;就变…

JVM 面经

1、什么是 JVM? JVM 就是 Java 虚拟机&#xff0c;它是 Java 实现跨平台的基石。程序运行之前&#xff0c;需要先通过编译器将 Java 源代码文件编译成 Java 字节码文件&#xff1b;程序运行时&#xff0c;JVM 会对字节码文件进行逐行解释&#xff0c;翻译成机器码指令&#x…

Ubuntu平台下安装Node相关环境

说明&#xff1a;在进行VUE、TS等开发需要用到NodeJS相关环境&#xff0c;不同的项目有时候需要不同的Node版本支撑。本文将详细讲解NVM、Node、Yarn、PM2等环境安装的实施步骤。 测试服务器环境&#xff1a;22.04 LTS。 1. NVM 定义&#xff1a;Node Version Manager&#x…

Windows Server 2025 使用 IIS 搭建 ASP.NET 3.5 网站

开启远程桌面 参考文章Windows server开启远程桌面教程打开服务管理器。ECS 配置安全组&#xff0c;开启 3389Telnet 验证网络联通性 telnet x.x.x.x 338安装 Windows App&#xff0c;登录验证 安装 ASP.NET 3.5 1.参考文章Windows Server 2012安装 .NET Framework 3.5和 Wi…

【强化学习】基于深度强化学习的微能源网能量管理与优化策略研究【Python】

目录 主要内容 程序要点 2.1 微能源网系统组成 2.2 强化学习及Q学习算法 部分代码 运行结果 下载链接 主要内容 该程序借助深度 Q 网络&#xff08;DQN&#xff09;&#xff0c;学习预测负荷、风 / 光可再生能源功率输出及分时电价等环境信息&#xff0c;运用…

楼宇自控借何种技术,驱动建筑迈向高效绿色

在全球积极倡导可持续发展的大背景下&#xff0c;建筑行业作为能源消耗和碳排放的大户&#xff0c;实现高效绿色发展迫在眉睫。楼宇自控系统凭借其先进的技术手段&#xff0c;成为推动建筑向高效绿色转型的关键力量。那么&#xff0c;楼宇自控究竟借助哪些技术&#xff0c;让建…

监控易一体化运维:监控易机房管理,打造高效智能机房

在数字化浪潮中&#xff0c;企业对数据中心和机房的依赖程度与日俱增&#xff0c;机房的稳定运行成为业务持续开展的关键支撑。信息化的变迁&#xff0c;见证了机房管理从传统模式向智能化、精细化转变的过程。今天&#xff0c;就为大家深度剖析监控易在机房管理方面的卓越表现…

PHP安装HTML转图片的扩展GD库的使用

修改你的PHP.ini文件,找到以下位置 ;extensionphp_gd2.dll 把前面的;去掉…

清华大学第10讲:迈向未来的AI教学实验396页PPT 探索未来教育的无限可能|附PPT下载方法

导 读INTRODUCTION 今天跟大家分享的是清华大学新闻与传播学院、人工智能学院双聘教授沈阳教授团队出品的《迈向未来的AI教学实验》课程作业集&#xff0c;随着人工智能技术的飞速发展&#xff0c;教育领域也迎来了前所未有的变革。该报告为沈阳教授与学生们在“迈向未来的AI教…

《白帽子讲 Web 安全》之服务端请求伪造(SSRF)深度剖析:从攻击到防御

引言 在当今复杂的网络环境中&#xff0c;Web 应用安全犹如一座时刻需要精心守护的堡垒。随着技术的不断演进&#xff0c;各类安全威胁层出不穷&#xff0c;其中服务端请求伪造&#xff08;SSRF&#xff09;正逐渐成为令开发者与安全从业者头疼的一大难题。吴翰清在《白帽子讲…

豪越消防一体化安全管控平台:消防管理智能化

在社会快速发展、城市建设日益复杂的今天&#xff0c;消防安全始终是保障人民生命财产安全、维护社会稳定的重要基石。传统消防管理模式在应对当下复杂多变的消防安全需求时&#xff0c;逐渐暴露出诸多局限性&#xff0c;而豪越消防一体化平台的出现&#xff0c;为消防管理领域…

瑞芯微RK356X主板复用接口配置方法,触觉智能嵌入式方案商

本文介绍瑞芯微RK356X系列复用接口配置的方法&#xff0c;基于触觉智能RK3562开发板演示&#xff0c;搭载4核A53处理器&#xff0c;主频高达2.0GHz&#xff1b;内置独立1Tops算力NPU&#xff0c;可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。 复…

NX二次开发刻字功能——预览功能

这个预览功能其实在NX软件中很常见,有利于建模者确定刻字的位置,这个功能早在唐康林老师的超级长方体教程中出现过。我只是学以致用。把该功能集成刻字中。 在勾选预览的同时,如果点击放大镜也就是显示预览结果,要刻字的对象透明度数值为70,同时预览结果文字会变成撤销,如…

容器主机CPU使用率突增问题一则

关键词 LINUX、文件系统crontab 、mlocate根目录使用率 There are many things that can not be broken&#xff01; 如果觉得本文对你有帮助&#xff0c;欢迎点赞、收藏、评论&#xff01; 一、问题现象 业务一台容器服务器&#xff0c;近期经常收到cpu不定期抖动告警&#x…