C++Primer对象移动
专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!
目录
- 13.6对象移动
- 13.6.1右值引用
- 左值持久;右值短暂
- 变量是左值
- 标准库move函数
- 13.6.2移动构造函数和移动赋值运算符
- 移动操作、标准库容器和异常
- 移动赋值运算符
- 移后源对象必须可析构
- 合成的移动操作
- 删除的函数,编译器就不会合成它们
- 拷贝些交换赋值运算符和移动操作
- Message类的移动操作
- 移动迭代器
- 13.6.3右值引用和成员函数宇
- 右值和左值引用成员函数
- 重载和引用函数
13.6对象移动
新标准的一个最主要的特性是可以移动而非拷贝对象的能力。在某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
如我们已经看到的,我们的StrVec类是这种不必要的拷贝的一个很好的例子。在重新分配内存的过程中,从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包含不能被共享的资源(如指针或IO缓冲)。因此,这些类型的对象不能拷贝但可以移动。
在旧C++标准中,没有直接的方法移动对象。因此,即侗不必拷贝对象的情况下,我们也不得不拷贝。如果对象较大,或者是对象本身要求分配内存空间(如string),进行不必要的拷贝代价非常高。类似的,在旧版本的标准库中,容器中所保存的类必须是可拷贝的。但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。
13.6.1右值引用
为了支持移动操作,新标准引入了一种新的引用类型一一右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质,们可以自由地将一个右值引用的资源“移动“到另一个对象中。
回忆一下,左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用(Ivaluereference),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
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的左值引用或者一个右值引用绑定到这类表达式上。
左值持久;右值短暂
考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。由于右值引用只能绑定到临时对象,我们得知
- 所引用的对象将要被销毁
- 该对象没有其他用户
这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
变量是左值
变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上,这有些令人惊讶:
int &&rr1 = 42;//正确:字面常量是右值
int &&rr2 = rr1;//错误:表达式rr1是左值!
其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕韶,变量是持久的,直至离开作用域时才被销毁。
标准库move函数
虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中.
int &&rr3 = std::mov(rr1); //OK
move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。
如前所述,与大多数标准库名字的使用不同,对move我们不提供using声明。我们直接调用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=nullptI;
}
我们将简短解释noexcept(它通知标准库我们的构造函数不抛出任何异常),但让我们先分析一下此构造函数完成什么工作。
与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行析构函数。StrVec的析构函数在first_free上调用deallocate。如果我们忘记了改变s.first_free,则销毁移后源对象就会释放掉我们刚刚移动的内存。
移动操作、标准库容器和异常
由于移动操作“窃取“资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
一种通知标准库的方法是在我们的构造函数中指明noexcept。noexcept是新标准引入的。目前重要的是要知道,noexcept是我们承诺一个函数不抛出异常的一种方法。我们在一个函数的参数列表后指定noexcept。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间:
class StrVec
public:
StrVec(StrVec&&) noexcept;//移动构造函数
//其他成员的定义,如前
StrVec::StrVec(StrVec&&s) noexcept:/*成员初始化器*/
{/*构造函数体*/}
我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept。
搞清楚为什么需要noexcept能帮助我们深入理解标准库是如何与我们自定义的类型交互的。我们需要指出一个移动操作不抛出异常,这是因为两个相互关联的事实:首先,虽然移动操作通常不抛出异常,但抛出异常也是允许的;其次,标准库容器能对异常发生时其自身的行为提供保障。例如,vector保证,如果我们调用pash_back时发生异常,vector自身不会发生改变。
现在让我们思考push_back内部发生了什么。类似对应的StrVec操作,对一个vector调用push_back可能要求为vector重新分配内存空间。当重新分配vector的内存时,vector将元素从旧空间移动到新内存中,就像我们在reallocate中所做的那样。
如我们刚刚看到的那样,移动一个对象通常会改变它的值。如果重新分配过程使用了移动构造函数,且在移动了部分而不是全部元素后抛出了一个异常,就会产生问题。旧穿间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在。在此情况下,vector将不能满足自身保持不变的要求。
另一方面,如果vector使用了拷贝构造函数且发生了异常,它可以很容易地满足要求。在此情况下,当在新内存中构造元素时,旧元素保持不变。如果此时发生了异常,vector可以释放新分配的(但还未成功构造的)内存并返回。vector原有的元素仍然存在。
为了避免这种潜在问题,除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数而不是移动构造函数。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为noexcept来做到这一点。
移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值:
StrVec &StrVec::operator=(StrVec&&rhs)noexcept
{//直接检测自赋值if(this1=_grhs)free();//释放已有元素elements=rhs.elements;//从rhs接管资源first_free = rhs.fitrst_free;cap=rhs.cap;//将rhs置于可析构状态rhs.elements=rhs.first_free=rhs.cap=nullptr;return*thts;
)
在此例中,我们直接检查this指针与rhs的地址是否相同。如果相同,右侧和左例运算对象指向相同的对象,我们不需要做任何事情。否则,我们释放左侧运算对象所使用的内存,并接管给定对象的内存。与移动构造函数一样,我们将rhs中的指针置为nullptr。
我们费心地去检查自赋值情况看起来有些奇怪。毕竟,移动赋值运算符需要右侧运算对象的一个右值。我们进行检查的原因是此右值可能是move调用的返回结果。与其他任何赋值运算符一样,关键点是我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同的资源)。
移后源对象必须可析构
从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。我们的StrVec的移动操作满足这一要求,这是通过将移后源对象的指针成员置为nullptr来实现的。
除了将移后源对象置为析构安全的状态之外,移动操作还必须保证对象仍然是有效一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据。
例如,当我们从一个标准库string或容器对象移动数据时,我们知道移后源对象仍然保持有效。因此,我们可以对它执行诺如empty或size这些操作。但是,我们不知道将会得到什么结果。我们可能期望一个移后源对象是空的,但这并没有保证。
我们的StrVec类的移动操作将移后源对象置于与默认初始化的对象相同的状态。因此,我们可以继续对移后源对象执行所有的StrVec操作,与任何其他默认初始化的对象一样。而其他内部结构更为复杂的类,可能表现出完全不同的行为。
合成的移动操作
与处理拷贝构造函数和拷贝赋值运算符一样,编译器也会合成移动构造函数和移动赋值运算符。但是,合成移动操作的条件与合成拷贝操作的条件大不相同。
回忆一下,如果我们不声明自己的拷贝构造函数或拷贝赋值运算符,编译器总会为我们合成这些操作。拷贝操作要么被定义为逐成员拷贝,要么被定义为对象赋值,要么被定义为删除的函数。
与拷贝操作不同,编译器根本不会为某些类合成移动操作。特别是,如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。因此,树些类就没有移动构造函数或移动赋值运算符。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编诙器才会为它合成移动构造函数或移动赋值运算符。编详器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员:
//编译器会为X和hasX合成移动操作
struct X{int i;//内置类型可以移动std::string s;//string定义了自己的移动操作
};
struct hasX{X mem;//X有合成的移动操作
};X x,x2 = std:amove(x);//使用合成的移动构造函数hasX hx,hx2= std::move(hx);//使用合成的移动构造出数
}
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们显式地要求编详器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则:
- 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
- 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
- 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
- 类似拷贝赋值运算符,如果有类成员是const的或是引用,则类的移动赋值运算符被定义为删除的。
例如,假定Y是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造函数:
//假定王是一个类,它定义了自己的拷贝构造函数但未定义自己的移动构造出数
struct hasY{hasY()=default;hasY(hasY&&)= default;Ymem;//hasY将有一个删除的移动构造函数
}
hasY hy,hy2=std::move(hy);//错误:移动构造函数是删除的
编译器可以拷贝类型为Y的对象,但不能移动它们。类hasY显式地要求一个移动构造函数,但编译器无法为其生成。因此,hasy会有一个删除的移动构造函数。如果hasy忽略了移动构造函数的声明,则编译器根本不能为它合成一个。如果移动操作可能被定义为
删除的函数,编译器就不会合成它们
移动操作和合成的拷贝控制成员间还有最后一个相互作用关系:一个类是否定义了自己的移动操作对拷贝操作如何合成有影响.如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
移动右值,拷贝左值……
如果一个类既有移动构造丽数,也有拷贝构造函数,编译器使用普通的函数匹配规则汝确定使用哪个构造函数。赋值操作的情况类似。例如,在我们的StrVec类中,拷贝构造函数接受一个constStrVec的引用。因此,它可以用于任何可以转换为StrVec的类型。而移动构造函数接受一个StrvVecgg,因此只能用于实参是(非static)右值的情形:
StrVecv vl,v2;
v1 = v2;//v2是左值;使用拷贝赋值
StrVec getVec(istream&);//gqetVec返回一个右值
v2=getVec(cin);//getVec(cin)是一个右值;使用移动赋值
在第一个赋值中,我们将v2传递给赋值运算符。v2的类型是strVec,表达式v2是一个左值。因此移动版本的赋值运算符是不可行的(参见6.6节,第217页),因为我们不能隐式地将一个右值引用绑定到一个左值。因此,这个赋值语句使用拷贝赋值运算符。
在第二个赋值中,我们赋予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的Foogg。Foo的拷贝构造函数是可行的,因为我们可以将一个Foogg转换为一个constFoog。因此,z的初始化将使用Foo的拷贝构造函数。
值得注意的是,用拷贝构造函数代替移动构造函数几乎肯定是安全的(赋值运算符的情况类似。一般情况下,拷贝构造函数满足对应的移动构造函数的要求:它会拷贝给定对象,并将原对象置于有效状态。实际上,拷贝构造函数甚至都不会改变原对象的值。
拷贝些交换赋值运算符和移动操作
我们的HasPtr版本定义了一个拷贝并交换赋值运算符,它是函数匹配和移动操作间相互关系的一个很好的示例。如果我们为此类添加一个移动构造函数,它实际上也会获得一个移动赋值运算符:
class HasPtr{
public:
//添加的移动构造函数
HasPtr(HasPtr&&p)noexcept:Ps(P.Ps)},工(p.i){p.ps=01)
//赋值运算符既是移动赋值运算符,也是指贝赋值运算符
HasPtr& operator=(HasPtr rhs)
{swap(*this,rhs);return*this;}
在这个版本中,我们为类添加了一个移动构造函数,它接管了给定实参的值。构造函数体将给定的HasPtz的指针置为0,从而确保销毁移后源对象是安全的。此函数不会抛出异常,因此我们将其标记为noexcept。
现在让我们观察赋值运算符。此运算符有一个非引用参数,这意味着此参数要进行拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数一一左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能。
//例如,假定hp和hp2都是HasPtr对象:
hp=hp2;//hp2是一个左值;hp2通过指贝构造函数来拷贝
hp=std::move(hp2);//移动构造函数移动hp2
在第一个赋值中,右侧运算对象是一个左值,因此移动构造函数是不可行的。rhs将使用拷贝构造函数来初始化。拷贝构造函数将分配一个新string,并拷贝hp2指向的string。
在第二个赋值中,我们调用std::move将一个右值引用绑定到hp2上。在此情况下,拷贝构造函数和移动构造函数都是可行的。但是,由于实参是一个右值引用,移动构造函数是精确匹配的。移动构造函数从hp2拷贝指针,而不会分配任何内存。
不管使用的是拷贝构造函数还是移动构造函数,赋值运算符的函数体都swap两个运算对象的状态。交换HasPtzr会交换两个对象的指针(及int)成员。在swap之后,rhs中的指针将指向原来左侧运算对象所拥有的string。当rhs离开其作用域时,这个string将被销毁。
Message类的移动操作
定义了自己的拷贝构造函数和拷贝赋值运算符的类通常也会从移动操作受益。例如,我们的Message和Folder类就应该定义移动操作。通过定义移动操作,Message类可以使用string和set的移动操作来避免拷贝contents和folders成员的额外开销。
但是,除了移动folders成员,我们还必须更新每个指向原Message的Folder。我们必须删除指向旧Message的指针,并添加一个指向新Message的指针。
移动构造函数和移动赋值运算符都需要更新Folder指针,因此我们首先定义一个操作来完成这一共同的工作:
//从本Message移动Folder指针
void Message::moye_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调用,代码仍能正常工作,但带来了不必要的拷贝。函数然后遍历所有FEolder,从其中别除指向原Message的指针并添加指向新Message的指针。
值得注意的是,向set插入一个元素可能会抛出一个异常一一向容器添加元素的操作要求分配内存,意味着可能会抛出一个bad_alloc异常。因此,与我们的HasPtr和StrVec类的移动操作不同,Message的移动构造函数和移动赋值运算符可能会抛出异常。因此我们未将它们标记为noexcept。
函数最后对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的指针。
移动赋值运算符直接检查自赋值情况:
MessagegMessage::operator=(Message&&rhs)
{if(this!=&rhs){//直接检查自赋值情况remove_from_Folders();contents=std::move(rhs.contents);//移动赋值运算符moye_Folders(5rhs);//重置Folders指向本Messagereturn*this;
}
与任何赋值运算符一样,移动赋值运算符必须销毁左侧运算对象的旧状态。在本例中,销毁左侧运算对象要求我们从现有folders中删除指向本Message的指针,我们调用remove_from_Folders来完成这一工作。完成别除工作后,我们调用move从rhs将contents移动到this对象。剩下的就是调用move_Messages来更新Folder指针了。
移动迭代器
StrVec的reallocate成员使用了一个for循环来调用construct从旧内存将元素拷贝到新内存中。作为一种荐换方法,如果我们能调用uninitialized_copy来构造新分配的内存,将比循环更为简单。但是,uninitialized_copy恰如其名:它对元素进行拷贝操作。标准库中并没有类似的函数将对象“移动“到未构造的内存中。
新标准库中定义了一种移动迭代器(moveiterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迪代器不同,移动迭代器的解引用运算符生成一个右值引用。
我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迪代器。
原迭代器的所有其他操作在移动迭代器中都照常工作。由于移动迭代器支持正常的追代器操作,我们可以将一对移动迭代器传递给算法。特别是,可以将移动迭代器传递给
uninitialized_copy:
void StrVec::reallocate()
{
//分配大小两倍于当前规模的内存空间
auto newcapacitty = size()?2*size():i;
auto first=alloc.allocate(newcapacity);
//移动元素
auto 1ast=uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);
free();//释放旧空间
elements=first;//更新指针
first_free = 1ast;
cap=elements+newcapactty;
}
uninitialized_copy对输入序列中的每个元素调用construct来将元素“拷贝“到目的位置。此算法使用迭代器的解引用运算符从输入序列中提取元素。由于我们传递给它的是移动迭代器,因此解引用运算符生成的是一个右值引用,这意味着construct将使用移动构造函数来构造元素。
值得注意的是,标准库不保证哪些算法适用移动迭代器,哪些不适用。由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户
13.6.3右值引用和成员函数宇
除了构造函数和赋值运算符之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式一一一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。
例如,定义了push_back的标准库容器提供两个版本:一个版本有一个右值引用参数,而另一个版本有一个const左值引用。假定X是元素类型,那么这些容器就会定义以下两个push_back版本:
void push_back(const X&)}//指贝:绑定到任意类型的X
void push_back(X&&);//移动:只能绑定到类型X的可修改的右值
我们可以将能转换为类型x的任何对象传递给第一个版本的push_back。此版本从其参数拷贝数据。对于第二个版本,我们只可以传递给它非const的右值。此版本对于非const的右值是精确匹配(也是更好的匹配)的,因此当我们传递一个可修改的右值时,编译器会选择运行这个版本。此版本会从其参数窃取数据。
一般来说,我们不需要为函数操作定义接受一个const X&&或是一个(普通的)X&参数的版本。当我们希望从实参“窃取“数据时,通常传递一个右值引用。为了达到这一目的,实参不能是const的。类似的,从一个对象进行拷贝的操作不应该改变该对象。因此,通常不需要定义一个接受一个(普通的)X&参数的版本。
作为一个更具体的例子,我们将为StrVec类定义另一个版本的push_back:
class StrVec
public:
void push_back(const std::stringg&);//拷贝元素
void push_back(std::string&g)}//移动元素//其他成员的定义,如前
//与13.5节(第466页)中的原版本相同
voidStrVec::push_back(conststringgs)
{
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&5)
这些调用的差别在于实参是一个左值还是一个右值(从“done“创建的临时string),具体调用哪个版本据此来决定。
右值和左值引用成员函数
通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值例如:
string s1="avalue",s2="another";
auto n = (s1+s2).ftnd('a');
此例中,我们在一个string右值上调用find成员(参见9.5.3节,第325页),该string右值是通过连接两个string而得到的。有时,右值的使用方式可能令人惊讶:
s1+s2 = "wow1";
此处我们对两个string的连接结果一一一个右值,进行了赋值。
在旧标准中,我们没有办法阻止这种使用方式。为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是,我们可能希望在自己的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即,this指向的对象〉是一个左值。
我们指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符(reference qualifier):
class Foo{
public:
Foo goperator=(const Foo&);//只能向可修改的左值赋值
//Foo的其他参数
Foo& Foo::operator=(const Foo&rhs)&
{
//执行将rhs赋予本对象所需的工作
return*this;
}
引用限定符可以是g或gg,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于(非static)成员函数,且必须同时出现在函数的声明和定义中。对于&限定的函数,我们只能将它用于左值;对于gg限定的函数,只能用于右值:
Foo&retFoo();“//返回一个引用;retFoo调用是一个左值
Foo retVal();//返回一个值;retVal调用是一个右值
Foo i,j;//i和j是左值
i=j;//正确:i是左值
retFoo()=i;//正确:retFoo()返回一个左值
retVal()=j;//错误:retVal()返回一个右值
i=retVal();//正确:我们可以将一个右值作为赋值操作的右侧运算对象
一个函数可以同时用const和引用限定.在此情况下,引用限定符必须跟随在const限定符之后:
class Foo{
public:
Foo someMem() & const;//错误 const限定符必须是左值
FooanotherMem()const&;//正确:const限定符在前
}
重载和引用函数
就像一个成员函数可以根据是否有const来区分其重载版本一样,引用限定符也可以区分重载版本。而且,我们可以综合引用限定符和const来区分一个成员函数的重载版本。例如,我们将为Foo定义一个名为data的vector成员和一个名为sorted的成员函数,sorted返回一个Foo对象的副本,其中vector已被排序:
class Foo{
public:
Foo sorted()&&}//可用于可政变的右值
Foo sorted()const&;//可用于任何类型的Foo
//Foo的其他成责的定义
private:
vector<int>data;
//本对象为右值,因此可以原址排序
FooFoo::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是出数类型的类型别名
//此出数类型可以用来比较int值1
using Comp=bool(const int&,const int&);
Foo sorted(Comp*);//正确:不同的参数列表
Foo sorted(Comp*)const;//正确:两个版本都没有引用限定符
}
本例中声明了一个没有参数的const版本的sorted,此声明是错误的。因为Foo类中还有一个无参的sorted版本,它有一个引用限定符,因此const版本也必须有引用限定符。另一方面,接受一个比较操作指针的sorted版本是没问题的,因为两个函数都没有引用限定符。
相关文章:
C++Primer对象移动
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
互联网三高-数据库高并发之分库分表ShardingJDBC
1 ShardingJDBC介绍 1.1 常见概念术语 ① 数据节点Node:数据分片的最小单元,由数据源名称和数据表组成 如:ds0.product_order_0 ② 真实表:再分片的数据库中真实存在的物理表 如:product_order_0 ③ 逻辑表:…...
七、自动化概念篇
自动化测试概念 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。通常,在设计了测试用例并通过评审之后,由测试人员根据测试用例中描述的过程一步步执行测试,得到实际结果与期望结果的比较。在此过程中,为了节省人…...
python操作mongodb
1、安装包 pyMongo是MongoDB官方推荐的Python驱动程序,它提供了访问MongoDB数据库所需的接口。安装PyMongo非常简单,可以通过pip包管理工具来安装最新版本: pip install pymongo 安装完成后,我们可以使用以下Python代码来检查是否…...
IS-IS中特殊字段——OL过载
文章目录 OL 过载位 🏡作者主页:点击! 🤖Datacom专栏:点击! ⏰️创作时间:2025年04月13日20点12分 OL 过载位 路由过载 使用 IS-IS 的过载标记来标识过载状态 对设备设置过载标记后ÿ…...
行星际激波在日球层中的传播:Propagation of Interplanetary Shocks in the Heliosphere (第二部分)
行星际激波在日球层中的传播:Propagation of Interplanetary Shocks in the Heliosphere (第一部分)-CSDN博客 Propagation of Interplanetary Shocks in the Heliosphere [ Chapter 3 ] [PDF: arXiv] 本文保留原文及参考文献,参…...
紫光同创FPGA实现HSSTLP光口视频点对点传输,基于Aurora 8b/10b编解码架构,提供6套PDS工程源码和技术支持
目录 1、前言工程概述免责声明 2、相关方案推荐我已有的所有工程源码总目录----方便你快速找到自己喜欢的项目紫光同创FPGA相关方案推荐我这里已有的 GT 高速接口解决方案Xilinx系列FPGA实现GTP光口视频传输方案推荐Xilinx系列FPGA实现GTX光口视频传输方案推荐Xilinx系列FPGA实…...
正则表达式使用知识(日常翻阅)
正则表达式使用 一、字符匹配 1. 普通字符 描述:直接匹配字符本身。示例: abc 匹配字符串中的 “abc”。Hello 匹配字符串中的 “Hello”。 2. 特殊字符 .(点号): 描述:匹配任意单个字符(…...
CSS padding(填充)学习笔记
CSS 中的 padding(填充)是一个非常重要的属性,它用于定义元素边框与元素内容之间的空间,即上下左右的内边距。合理使用 padding 可以让页面布局更加美观、清晰。以下是对 CSS padding 的详细学习笔记。 一、padding 的作用 padd…...
Python中的字符串、列表、字典和集合详解
Python是一门强大的编程语言,其内置的数据结构丰富多样。其中,字符串、列表、字典和集合是最常用的数据类型。本文将对它们的区别、用法和使用场景进行详细介绍,帮助大家更好地理解和应用这些数据结构。 1. 字符串(String…...
【CUDA编程】CUDA Warp 与 Warp-Python 对比文档
相关文档: Nvidia-Warp GitHub:nvidia/warp CUDA Warp 和 Warp-Python 库 的对比与统一文档,涵盖两者的核心概念、区别、使用场景及示例: 1. CUDA Warp(硬件/编程模型概念) 1.1 定义与核心概念 定义: C…...
中厂算法岗面试总结
时间:2025.4.10 地点:上市的电子有限公司 面试流程: 1.由负责人讲解公司文化 2,由技术人员讲解公司的技术岗位,还有成果 3.带领参观各个工作位置,还有场所 4.中午吃饭 5.面试题,闭卷考试…...
Losson 4 NFS(network file system(网络文件系统))
网络文件系统:在互联网中共享服务器中文件资源。 使用nfs服务需要安装:nfs-utils 以及 rpcbind nfs-utils : 提供nfs服务的程序 rpcbind :管理nfs所有进程端口号的程序 nfs的部署 1.客户端和服务端都安装nfs-utils和rpcbind #安装nfs的软件rpcbind和…...
自动化运行后BeautifulReport内容为空
一、问题描述 自动化程序运行后发现运行目录下生成的html报告文件内容为空,没有运行结果。 二、测试环境 操作系统:Windows 11 家庭版BeautifulReport:0.1.3Python:3.11.9Appium-Python-Client:5.0.0Appium Server:2.…...
CTF-WEB排行榜制作
CTF-WEB排行榜制作 项目需求: 现在14道题对应有14个flag,我需要使用dockerfile搭建一个简单的,能够实现验证这些题目对应的flag来计分的简单网站(要求页面比较精美) 前十题设置为10分 11-14题设置为20分 1. flag{5a3…...
架构生命周期(高软57)
系列文章目录 架构生命周期 文章目录 系列文章目录前言一、软件架构是什么?二、软件架构的内容三、软件设计阶段四、构件总结 前言 本节讲明架构设计的架构生命周期概念。 一、软件架构是什么? 二、软件架构的内容 三、软件设计阶段 四、构件 总结 就…...
STM32单片机定时器的输入捕获和输出比较
目录 一、定时器的输入捕获 1、工作原理 2、示例代码 二、定时器的输出比较 1、工作原理 2、示例代码 三、总结 在STM32单片机中,定时器是一个非常重要的外设,广泛应用于时间管理、事件计时、波形生成等多种场景。其中输入捕获和输出比较是两个基…...
计算机组成原理-系统总线
1. 系统总线的定义 系统总线是计算机系统中各功能部件(CPU、存储器、I/O设备等)之间传递信息的公共通路,遵循统一的电气规范和时序协议,是计算机硬件互联的基础。 核心作用:实现数据、地址和控制信号的传输ÿ…...
【android bluetooth 框架分析 02】【Module详解 3】【HciHal 模块介绍】
1. 背景 我们在 gd_shim_module 介绍章节中,看到 我们将 HciHal 模块加入到了 modules 中。 modules.add<hal::HciHal>();在 ModuleRegistry::Start 函数中我们对 加入的所有 module 挨个初始化。 而在该函数中启动一个 module 都要执行那下面几步ÿ…...
Git 远程仓库
Git 入门笔记 远程仓库 Git 远程仓库 Git 远程仓库是一个托管在网络服务器上的代码仓库,它是团队协作开发的核心。 通过远程仓库,开发者可以共享代码、同步更新,实现分布式协作。 SSH 密钥 SSH 密钥可以让你在使用 Git 时安全地连接远程…...
209.长度最小的子数组- 力扣(LeetCode)
题目: 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 示例 1:…...
符号右移“ >>= “ 与 无符号右移“ >>>= “ 的区别
符号右移" >> " 与 无符号右移" >>> " 的区别 一、符号右移" >> " 与 无符号右移" >>> " 的区别1. 符号右移(>>)与无符号右移(>>>)的区别…...
山东大学软件学院项目实训-基于大模型的模拟面试系统-专栏管理部分
本周我的主要任务是关于专栏管理部分的完善。 专栏图片的显示问题 问题分析 根据代码可知:图片URL来自于portfolio.headImgUrl,而且如果URL不存在的话,应该显示的是无图片,而网页中显示加载失败说明portfolio.headImgUrl应该是存…...
从 SYN Flood 到 XSS:常见网络攻击类型、区别及防御要点
常见的网络攻击类型 SYN Flood、DoS(Denial of Service) 和 DDoS(Distributed Denial of Service) 是常见的网络攻击类型,它们的目标都是使目标系统无法正常提供服务。以下是它们的详细说明: 1. SYN Flood…...
ros2-rviz2控制unity仿真的6关节机械臂,探索从仿真到实际应用的过程
文章目录 前言(Introduction)搭建开发环境(Setup Development Environment)在window中安装Unity(Install Unity in window)创建Docker容器,并安装相关软件(Create Docker containers…...
01_通过调过api文字生成音频示例
第1 第2 第3,测试音色 第4 第5 第6 第7生成api_key 第8代码 import requestsurl "https://api.siliconflow.cn/v1/audio/speech"payload {"input": "在中国传统文化中,谦让被视为一种美德,但过度的让步…...
使用PyTorch实现目标检测边界框转换与可视化
一、引言 在目标检测任务中,边界框(Bounding Box)的坐标表示与转换是核心基础操作。本文将演示如何: 实现边界框的两种表示形式(角点坐标 vs 中心坐标)之间的转换 使用Matplotlib在图像上可视化边界框 验…...
QEMU学习之路(8)— ARM32通过u-boot 启动Linux
QEMU学习之路(8)— ARM32通过u-boot 启动Linux 一、前言 参考文章: Linux内核学习——内核的编译和启动 Linux 内核的编译和模拟执行 Linux内核运行——根文件系统 Linux 内核学习——使用 uboot 加载内核 二、构建Linux内核 1、获取Linu…...
flutter 桌面应用之右键菜单
在 Flutter 桌面应用开发中,context_menu 和 contextual_menu 是两款常用的右键菜单插件,各有特色。以下是对它们的对比分析: 🧩 context_menu 集成方式:通过 ContextMenuArea 组件包裹目标组件,定义…...
系统设计面试总结:高性能相关:CDN(内容分发网络)、什么是静态资源、负载均衡(Nginx)、canal、主从复制
以下为本人自学回顾使用,请支持javaGuide原版。 1.CDN概述 CDN 就是将静态资源分发到多个不同的地方以实现就近访问,进而加快静态资源的访问速度,减轻服务器以及带宽的负担。 你可以将 CDN 看作是服务上一层的特殊缓存服务,分布…...
从红黑树到哈希表:原理对比与典型场景应用解析(分布式以及布隆过滤器)
在数据结构的世界里,红黑树一直以「自平衡二叉查找树」的身份备受赞誉。凭借红黑节点的精妙设计,它能将插入、删除、查找的时间复杂度稳定控制在 ( log n ) (\log n) (logn),成为处理有序数据的经典方案。然而,当业务场景对「…...
动手学深度学习:手语视频在VGG模型中的测试
前言 其他所有部分同上一篇AlexNet一样,所以就不再赘诉,直接看VGG搭建部分。 模型 VGG是第一个采取块进行模块化搭建的模型。 def vgg_block(num_convs,in_channels,out_channels):layers[]for _ in range(num_convs):layers.append(nn.Conv2d(in_ch…...
微信小程序实战案例 - 餐馆点餐系统 阶段 4 - 订单列表 状态
✅ 阶段 4 – 订单列表 & 状态 目标 展示用户「我的订单」列表支持状态筛选(全部 / 待处理 / 已完成)支持分页加载和实时刷新使用原生组件编写 ✅ 1. 页面结构:文件结构 pages/orders/├─ index.json├─ index.wxml├─ index.js└─…...
深度学习理论-直观理解 Attention
本文首先介绍 Attention 的原始公式,然后以 Self-Attention 为例,简化后逐步分析 Attention 计算结果表达的含义 Attention Attention 公式如下: A t t e n t i o n s o f t m a x ( Q ⋅ K T d k ) ⋅ V Attention softmax(\frac{Q \cd…...
python中 “with” 关键字的取舍问题
自动管理资源(自动关闭文件) 当你使用 with 打开文件时,文件会在 with 代码块结束后自动关闭,无论是否发生异常。这意味着你不需要显式地调用 f.close() 来关闭文件 示例: with open("words.txt", "r…...
ISIS协议(动态路由协议)
ISIS基础 基本概念 IS-IS(Intermediate System to Intermediate System,中间系统到中间系统)是ISO (International Organization for Standardization,国际标准化组织)为它的CLNP(ConnectionL…...
llm开发框架新秀
原文链接:https://i68.ltd/notes/posts/20250404-llm-framework3/ google开源ADK-Agent Development Kit 开源的、代码优先的 Python 工具包,用于构建、评估和部署具有灵活性和控制力的复杂智能体项目仓库:https://github.com/google/adk-python 2.6k项目文档:Age…...
Zookeeper的典型应用场景?
大家好,我是锋哥。今天分享关于【Zookeeper的典型应用场景?】面试题。希望对大家有帮助; Zookeeper的典型应用场景? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 ZooKeeper 是一个开源的分布式协调服务,主要用于管理和协调大…...
【C数据结构】 TAILQ双向有尾链表的详解
TAILQ双向有尾链表的详解 常见的链表结构1.SLIST2.STAILQ3.LIST4.TAILQ5.CIRCLEQ 一、TAILQ链表简介二、TAILQ的定义和声明三、TAILQ队列的函数1.链表头的初始化2.获取第一个节点地址3.获取最后一个节点地址4.链表是否为空5.下一个节点地址6.上一个节点地址7.插入头节点8.插入尾…...
redisson的unlock方法
//分布式方式,分布式锁,采用redisson锁 RLock lock redissonClient.getLock(userId.toString());//lock方法会无限重试。getLock底层是hash,大key是userid,小key是线程,value是重入次数 try {//boolean b lock.tryLo…...
ffmpeg 切割视频失败 ffmpeg 命令参数 -vbsf 在新版本中已经被弃用,需要使用 -bsf:v 替代
ffmpeg 切割视频失败 ffmpeg 命令参数 -vbsf 在新版本中已经被弃用,需要使用 -bsf:v 替代 从日志中可以看到问题出在第一个 ffmpeg 命令执行时: Unrecognized option vbsf.Error splitting the argument list: Option not found这是因为 ffmpeg 命令参…...
设计模式——抽象工厂模式总结
理解了前面的工厂模式后,再理解抽象工厂模式就很容易了。 工厂模式:https://blog.csdn.net/inside802/article/details/147170118?spm1011.2415.3001.10575&sharefrommp_manage_link 抽象工厂模式就是工厂模式的更加抽象化,父类不仅不承…...
JavaScript 定时器
在 JavaScript 中,定时器是实现代码在特定时间间隔执行或延迟执行的重要工具。下面我们将深入探讨定时器的相关知识。 定时器基础 setTimeout() setTimeout() 函数用于在指定的延迟时间后执行一次回调函数。它接受两个参数,第一个参数是要执行的回调函…...
企业经营决策风险
在企业的经营过程中,领导者每天都在面对大量的决策——该扩大生产还是收缩业务?该增设销售渠道还是提升产品质量?但你知道吗,企业最大的成本,不是生产成本,也不是人工成本,而是决策错误的成本&a…...
【云安全】云原生-centos7搭建/安装/部署k8s1.23.6单节点
一、节点基本配置 1、准备操作系统 2、 修改主机名 hostnamectl set-hostname master-1 hostnamectl set-hostname node1 hostnamectl set-hostname node2#验证hostnamectl status 3、修改/etc/hosts cat <<EOF >>/etc/hosts 192.168.255.137 master-1 192.168…...
【已更新完毕】2025泰迪杯数据挖掘竞赛B题数学建模思路代码文章教学:基于穿戴装备的身体活动监测
基于穿戴装备的身体活动监测 摘要 本研究基于加速度计采集的活动数据,旨在分析和统计100名志愿者在不同身体活动类别下的时长分布。通过对加速度数据的处理,活动被划分为睡眠、静态活动、低强度、中等强度和高强度五类,进而计算每个志愿者在…...
力扣每日打卡 1922. 统计好数字的数目 (中等)
力扣 1922. 统计好数字的数目 中等 前言一、题目内容二、解题方法1. 暴力解法(会超时,此法不通)2. 快速幂运算3. 组合计数的思维逻辑分析组合计数的推导例子分析思维小结论 4.官方题解4.1 方法一:快速幂 三、快速幂运算快速幂运算…...
宝塔Linux面板 - 添加站点建站时没有域名实现 IP 地址访问测试(宝塔面板建站 IP 访问)
前言 使用面板添加站点时,必须要填写一个域名用来指向程序,没有域名怎么办? 答案:域名直接写 【服务器 IP 地址】 操作步骤 如果还没有添加站点,则直接在创建站点的时候,域名那填写服务器地址即可&#…...
【GitHub探索】mcp-go,MCP协议的Golang-SDK
近期大模型Agent应用开发方面,MCP的概念比较流行,基于MCP的ToolServer能力开发也逐渐成为主流趋势。由于笔者工作原因,主力是Go语言,为了调研大模型应用开发,也接触到了mcp-go这套MCP的SDK实现。 对于企业内部而言&am…...
手撕TCP内网穿透及配置树莓派
注意: 本文内容于 2025-04-13 15:09:48 创建,可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容,请访问原文地址:手撕TCP内网穿透及配置树莓派。感谢您的关注与支持! 之前入手了树莓派5,…...