本章学习如何控制类类型对象在拷贝、赋值、移动和销毁时应该做什么。
一个类定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。他们分别是拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值函数和析构函数。
拷贝、赋值与销毁
拷贝构造函数
如果构造函数的第一个参数是自身对象的引用,而且其他额外参数都有默认值,则该构造函数是拷贝构造函数。
12345 | class {public: Foo(); Foo(const Foo&);} |
拷贝构造函数会在几种情况下被隐式地使用,所以不应该是explicit
。
合成拷贝构造函数
如果我们没有定义一个拷贝构造函数,则编译器会为我们定义一个。编译器将类内的每个非static
成员拷贝到正在创建的对象中。对于类类型的成员,则使用其拷贝构造函数来拷贝。
123456789 | class Sales_data{public: Sales_data(const Sales_data&);private: string bookNo; int units_sold; double revenue;} |
现在我们可以理解直接初始化和拷贝初始化的真正区别了,也就是说直接初始化是一个构造函数参数匹配的过程,而拷贝初始化要求我们将右侧的运算对象拷贝到左侧的对象中去,有时候还要求类型转换。
拷贝初始化不仅发生在使用=
运算符时,还发生在以下情况:
- 将一个对象作为实参传入形参
- 非引用返回类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素
拷贝构造函数用来初始化非引用类型的参数,这就是为什么拷贝构造函数的参数必须是引用的元素,不然为了调用构造函数又要拷贝实参,为了拷贝实参,又要调用构造函数,无限循环。
编译器可以跳过拷贝构造函数:
12 | string null_book="0-0000-00";string null_book("0-0000-00"); |
但是在这个节点上,拷贝构造函数必须是存在且可以访问的。(不是private
)
拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,则编译器自己合成一个。
运算符函数接受一个与自己所在类相同类型的函数:
12345 | class {public: Foo& operator=(const Foo&);} |
通常标准库要求容器的元素类型有自己的赋值运算符。
如果一个类没有定义拷贝赋值运算符,则编译器会合成一个。
析构函数
在析构函数中,首先执行函数体,然后按成员初始化的逆序销毁成员。
销毁类类型的成员,调用自己的析构函数,销毁内置类型成员什么也不做。其次,隐式销毁一个内置指针类型的成员不会delete
它指向的对象。
对象在以下情况执行构造函数:
举个例子:
123456789 | //新作用域{ Sales_data *p=new Sales_data; Sales_data p1(*p); shared_ptr p3=make_shared<Sales_data>(); vector<int> ivec; ivec.push_back(1); delete p;} |
在这里,所有的对象都成功被释放,其中p1
,p3
,ivec
都是局部对象。内置指针使用delete
进行释放。
特别注意当指向一个对象的引用或者指针离开作用域时,析构函数不会执行。
如果一个类未定义自己的析构函数时,编译器定义合成的析构函数,下面的代码等价于合成的析构函数:
12345 | class Sales_data{public: ~Sales_data() { }}; |
要认识到析构函数的函数体自身并不直接析构成员。函数体是用于执行销毁操作的另一部分。
三五法则
需要析构函数的类也不要拷贝和赋值操作
我们使用HasPtr
类,这个类在构造函数中分配动态内存,但是合成析构函数不会自动delete
一个指针,所以要自己添加一个析构函数:
123456 | class HasPtr{public: HasPtr(string& s):ps(new string(s)),i(0) {} ~HasPtr() {delete ps;}} |
这里只定义了自己的析构函数,所以编译器还会定义合成的拷贝构造函数和合成的拷贝赋值运算符。但是会引发一个严重的错误:
12345 | HasPtr f(HasPtr hp){ HasPtr ret=hp; return ret;} |
在这里由于ret
使用了合成的拷贝赋值运算符,所以有两个相同的指针指向动态内存,所以在离开函数作用域时,两个对象都被销毁,与此同时delete
操作被执行了两次。
并且在函数调用的时候,也发生了拷贝:
12 | HasPtr p("some values");p1=f(t); |
在t
被释放时,由于t
和p
内有相同的指针值,所以会p
内的指针会指向无效内存。
所以一个类如果要自定义析构函数,那么它也需要自定义拷贝构造函数和拷贝赋值运算符。
需要拷贝构造函数的类也需要拷贝赋值操作
某些类只需要定义拷贝或赋值操作,而不需要定义析构函数。
举个例子,如果某些类在创建时需要为每个对象创建独一无二的序号,包括在使用拷贝构造函数时,所以就需要我们自定义拷贝构造函数,但是可以看出我们不需要自定义析构函数。
使用=default
我们也可以声明拷贝构造函数为default
,来告诉编译器生成合成的版本。
123456789 | class Sales_data{public: Sales_data(); Sales_data(const Sales_data&)=default; Sales_data& operator=(const Sales_data&); ~Sales_data()=default;}Sales_data& Sales_data::operator=(const Sales_data&)=default; |
我们只能对具有合成的版本的成员函数使用default
。
阻止拷贝
有些类比如iostream
禁止拷贝对象,我们第一时间想到的是不去定义拷贝构造函数和运算符,但是不要忘了编译器会生成合成的版本。
定义删除的函数
新标准下我们将拷贝构造函数和运算符定义为删除的函数来阻止拷贝。我们虽然声明了它,但是却不能使用它,这时它的一种特性。
1234567 | struct NoCopy{ NoCopy(); NoCopy(const NoCopy&)=delete; NoCopy& operator=(const NoCopy&)=delete; ~NoCopy()=default;} |
=delete
声明必须是在函数第一次声明的时候,而且可以对任何成员函数声明=delete
,在有些情况下有利于编译器进行我们希望的函数匹配。
我们不能删除析构函数,如果析构函数是删除的,那么就不能销毁对象,同样的,如果某个成员的析构函数是删除的,那么成员所在的类对象也不能销毁,因为该成员无法销毁,整个对象也就无法销毁。
对于析构函数是删除的类,我们可以动态的分配对象,但是不能释放它:
123456789 | struct NoDtor{public: Dtor()=default; ~Dtor()=delete; }Dtor dt; //错误,析构函数是删除的Dtor *p=new Dtor();delete p; //错误,不能释放对象 |
合成的拷贝构造函数可能是删除的
对某些类来说,编译器将把这些合成的成员定义为删除的函数:
本质上,这些规则的要义在于,如果有数据成员不能默认构造、拷贝、赋值或者销毁时,对应的成员函数定义为删除的。
对于具有引用成员或者无法默认构造的const
成员的类,定义合成默认构造函数为删除是正确,并且因为合成的拷贝赋值运算符会试图赋值所有成员,这对于const
成员来讲是不可能的。
而对于引用成员来讲,将新值赋予一个引用成员,这样做改变的是引用的对象的值,而不是引用本身。
private拷贝控制
在新标准之前,可以将拷贝构造函数和拷贝赋值运算符定义为private
来阻止拷贝:
12345678 | class PrivateCopy{ PrivateCopy(const PrivateCopy&); PrivateCopy& operator=(const PrivateCopy&);public: PrivateCopy()=default; ~PrivateCopy();} |
由于构造函数和拷贝赋值运算符是private
的,用户代码将不能拷贝对象,但是友元和成员函数可以做到。为了阻止友元和成员函数进行拷贝,我们声明为private
,但是不定义它,这种做法是合法的,友元和成员函数在试图使用这些函数和运算符时,会发生链接错误。
拷贝控制和资源管理
管理类外资源必须定义拷贝控制成员,为了定义这些成员,我们先要确定拷贝的语义。
如果类的行为像值,改变副本不会对拷贝的对象产生任何影响;如果类的行为像指针,副本和源对象使用相同的底层数据。
标准库容器和string
的行为像值,shared_ptr
像指针,IO
和unique_ptr
则不允许拷贝和赋值。
我们定义一个HasPtr
类,成员有一个·int
和一个string
指针,通常类直接拷贝内置成员(不包括指针),这些成员本身就是值,所以应该让他们的行为看起来像值。所以我们如何拷贝指针成员决定了HasPtr
类的行为类似值还是指针。
行为像值的类
类值的版本如下:
1234567891011 | class HasPtr{public: HasPtr(const string& s=new string()):ps(new string(s)),i(0) {} HasPtr(const HasPtr& pn):ps(new string(*pn.ps)),i(pn.i) {} HasPtr& operator=(const HasPtr& pn); ~HasPtr() {delete ps;};private: string *ps; int i;} |
之所以该类类值,是因为在拷贝指针时,先创建拷贝对象的string
的副本,然后再让该对象的指针指向这个副本。
赋值运算符通常结合了析构函数和构造函数的操作,类似析构函数,赋值会销毁左侧运算对象的资源,类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
接下来给出拷贝赋值运算符的定义,在这里我们先拷贝右侧运算对象的资源,然后处理自赋值情况:
12345678 | HasPtr& operator=(const HasPtr& pn){ autp newp=new string(*pn.ps); delete ps; ps=newp; i=pn.i; return *this;} |
这里我们拷贝完资源后,释放旧的内存,把newp
的值赋予旧的ps
。这样保证不会出现异常,即使是拷贝自身对象也是一样。
如果我们这样定义:
1234567 | HasPtr& operator=(const HasPtr& pn){ delete ps; ps=new string(*(pn.ps)); i=pn.i; return *this;} |
如果我们自赋值的话,在进行拷贝之前自身对象的资源就已经被释放,拷贝的将是未定义的内存!
定义行为像指针的类
对于行为像指针的类,我们需要拷贝指针成员本身而不是它指向的string
。
为了使类的行为类值的最好办法是使用shared_ptr
类,拷贝一个shared_ptr
会拷贝它所包含的指针,在本例中我们不能单方面的释放指针关联的string
,只有当最后一个指向string
的HasPtr
被销毁时,才可以释放string
,因为多个对象是在共享string
的。
这里我们不使用shared_ptr
,而是设计自己的引用计数,引用计数的工作方式如下:
这里注意计数器不能直接作为HasPtr
的成员,不然:
123 | HasPtr p1;Hasptr p2(p1);HasPtr p3(p1); |
这里显然有三个对象在共享数据,但是p3
和p1
更新了数据,p2
的计数器如何更新?
解决的方法是把计数器放在动态内存中,使用指针来绑定它,当拷贝对象时,我们拷贝指向计数器的指针,然后对指针引用的计数器自增。
123456789101112 | class HasPtr{public: HasPtr(const string& s=new string()):ps(new string(s)),i(0),use(new size_t(1)) {} HasPtr(const HasPtr& p):ps(p.ps),i(p.i),use(p.use) {*use++;} HasPtr& operator=(const HasPtr&); ~HasPtr();private: string *ps; int i; size_t *use;} |
在这里,构造函数分配新的string
,并且将use
指向的计数器置1
,接着拷贝构造函数拷贝三个数据成员,并且通过指针递增计数器。
类指针的拷贝成员篡改引用计数
在定义析构函数时,我们应该注意析构函数不能无条件地delete ps
,因为还有其他对象在共享这个string
,所以析构函数应该递减这个计数器:
12345678 | HasPtr::~HasPtr(){ if(--*use==0) { delete ps; delete use; }} |
对于拷贝构造函数,它必须做到递增右侧对象的引用计数,同时削减左侧运算对象的引用计数,同时还要处理自赋值的情况:
12345678910111213 | HasPtr& operator=(const HasPtr& rhs){ ++*rhs.use; if(--*use==0) { delete ps; delete use; } ps=rhs.ps; use=rhs.use; i=rhs.i return *this;} |
这个函数中,首先自增右侧运算对象的引用计数,然后检查是否有其他用户,如果没有其他用户,则释放左侧运算对象的资源,如果有意味着传入的是自身对象,这时就不能释放资源。
交换操作
另外管理资源的类还会定义一个swap
函数,swap
的大致操作就是一次拷贝,两次赋值。这对于类来讲也是一样。
123 | HasPtr temp=v1;v1=v2;v2=temp; |
但是这样做太消耗资源,我们采用交换指针的方式即可:
123 | string *temp=v1.ps;v1.ps=v2.ps;v2.ps=temp; |
编写自己的swap
1234567891011 | class HasPtr{ friend void swap(HasPtr&,HasPtr&);};inlinevoid swap(HasPtr& p1,HasPtr& p2){ using std::swap; swap(p1.ps,p2.ps); swap(p1.i,p2.i);} |
另外注意,这里调用的swap
最好的自己定义的swap
而不是标准库的swap
,因为这个例子进行的是内置类型的swap
,所以可以使用标准库的版本,但是有的类不希望使用标准库的swap
来进行交换,比如一个HasPtr
成员h
,那么标准库的swap
就会对这个成员的string
造成不必要的拷贝。
所以正确的swap
函数应该是这样;
12345 | void swap(Foo& lhs,Foo& rhs){ using std::swap; swap(lhs.h,rhs.h);} |
这里的swap
未加std
限定。
在赋值运算符中使用swap
定义了swap
的类通常使用swap
来定义他们的赋值运算符,这些运算符使用了一种拷贝并交换的技术,这种技术将左侧与右侧的副本进行交换:
12345 | HasPtr& operator=(HasPtr rhs){ swap(*this,rhs); return *this;} |
注意这里运算符的参数并不是引用,意味着我们传入的是rhs
的副本,那么进行swap
就不会影响到实参,并且this
现在指向了rhs
的副本的资源。
而且我们也不用担心,rhs
的副本资源未被释放的问题,因为rhs
是一个局部变量。
拷贝控制示例
我们通过定义Message
类和Folder
类来运用拷贝控制,不光是资源管理,还能实现其他操作。
Message
在任意时刻只能有一个副本,一个Message
可以出现在多个Folder
中,如果一个Message
发生改变,那么在其他Folder
中我们也能够看到它的该表。
为了记录每个Message
位于哪些Folder
中,每一个Message
都会保存一个它所在的Folder
的指针的set
,每一个Folder
都会保存它含有的Message
的指针的set
。
Message
类提供save
和remove
操作,来向指定的Folder
中添加message或者删除message。创建一个新的Message
时,我们不会指出Folder
,只有使用save
操作。
我们拷贝Message
时,副本和原对象将是两个不同的对象,所以拷贝操作包括拷贝消息内容和所在的Folder
的指针的set
,此外我们还需要在所在的Folder
添加指向这个新拷贝对象的指针。
我们销毁Message
时,必须从所在的Folder
中删除其指针。
对于赋值情况,Folder
要删除左侧运算对象的指针,增加右侧运算对象的指针。
我们定义两个private
的工具函数来完成从指定的Folder
添加和删除Message
的操作。
Message类
12345678910111213141516 | class Message{ friend class Folder;public explicit Message(const string& str=""):contents(str) {} Message(const Message&); Message& operator=(const Message&); ~Message(); void save(Folders&); void remove(Folders&);private; string contents; set<Folders*> folders; void add_to_Folders(const Messages&); //向指定的Folder添加本Message void remove_from_Folders(); //从所在的所有Folder中删除本Message} |
接受一个string
参数的构造函数由于有一个默认参数,所以它也是默认构造函数。
save和remove成员
12345678910 | void Message::save(Folder& f){ folders.insert(f); f.addMsg(this);}void Message.remove(Folder& f){ folders.erase(f); f.remMsg(this);} |
Message类的拷贝控制成员
我们拷贝一个Message
时,我们应当向每一个所在的Folder
添加一个副本。所以我们要遍历set
,向每一个Folder
添加指向新Message
指针:
12345 | void Message::add_to_Folders(const Message& m){ for(auto f:m.folders) f->addMsg(this); //向该Folder添加指向本Message的指针} |
接着是拷贝构造函数:
1234 | Message::Message(const Message& m):contents(m.contents),folders(m.folders){ add_to_Folders(m);} |
Message类的析构函数
我们先定义的移除函数:
12345 | void remove_from_Folders(){ for(auto f:folders) f->remMsg(this);} |
接着编写析构函数
1234 | Message::~Message(){ remove_from_Folders();} |
Message的拷贝赋值运算符
12345678 | Message& Message::operator=(const Message& m){ remove_from_Folders(); contents=m.contents; folders=m.folders; add_to_Folders(rhs); return *this;} |
如果是自赋值的情况的话,这里如果先调用add_to_Folders
的话,后面再调用remove_from_Folders
的话,就会删除掉自身的Folder
的set
,这显然是错误的。
Message类的swap函数
通过定义自己的swap
函数可以避免不必要的拷贝,但是swap
函数必须管理指向被拷贝对象的Folder
的指针,我们通过两次扫描来完成这项工作:
12345678910111213 | void swap(Message& lhs,Message& rhs){ for(auto f:lhs.folders) f->remMsg(lhs); for(auto f:rhs.folders) f->remMsg(rhs); swap(lhs.contents,rhs.contents); swap(lhs.folders,rhs.folders); for(auto f:lhs.folders) f->addMsg(lhs); for(auto f:rhs.folders) f->addMsg(rhs);} |
我们通过两边扫描先从所在的每个Folder
中删除消息,然后交换,最后将交换后的Message
填充到交换后的Folder
当中。
动态内存管理类
这一节是自己实现vector
的简化版本,实现过程可以参考书,设计思想特别有用。
对象移动
我们现在可以使用移动对象而不是使用无必要的拷贝,并且对于那些不能拷贝的对象比如IO
类型和unique_ptr
来说,这些对象只能移动而不能拷贝。
右值引用
为了支持移动操作,新标准引入了右值引用,就是必须绑定到右值的引用,通过&&
来获取右值的引用,右值引用只能绑定到即将销毁的对象,所以我们可以自由地移动右值引用的资源。
而对于常规引用我们可以成为左值引用,不能将其绑定到要求转换的表达式,字面常量或者是返回右值的表达式。而右值引用具有完全相反的特性:
12345 | int i=114;int &ri=i; //正确int &ri=i*514; //错误,不能绑定到左值int &&ri=i*514; //正确const int &ri=i*514; //正确,可以将const的引用绑定到右值上 |
返回左值引用的函数,还有赋值,下标,解引用,前置递增/递减运算符都是返回左值的表达式。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们可以将一个const
左值引用或者右值引用绑定到这类表达式上。
右值引用的对象只能绑定到临时对象,所以所引用的对象将要被销毁,该对象没有其他用户。
要注意,变量也是一种左值,包括右值引用变量:
12 | int &&ri=42;int &&rri=ri; //错误 |
其实很容易理解,ri
接管了42
这一资源,那么它的生存期就是可持续的,也就是左值。
标准库move函数
尽管不能将一个右值引用绑定到一个左值上,但是可以显式地将左值转换为对应的右值引用类型。这时可以使用move
函数:
1 | int &rr2=std::move(rr1); |
在调用了move
之后,告诉编译器有一个左值,我们希望以右值的方式处理它,我们除了对rr1
赋值或者销毁,不再使用它。
移动构造函数和移动赋值运算符
我们为类定义移动构造函数和移动赋值运算符,这些成员从其他对象“窃取”资源,而不是拷贝资源。
例如我们为StrVec
类定义这样的成员:
12345 | StrVec::StrVec(StrVec&& s) noexcept :elements(s.elements),first_free(s.first_free),cap(s.cap){ s.elements=s.first_free=s.cap=nullptr;} |
这里的构造函数初始化列表接管s
的资源,然后将给定对象的指针设置为空。
移动操作,标准库容器和异常
noexcept
是我们承诺一个函数不抛出异常的方法,在一个构造函数中,noexcept
出现在函数的参数列表之后与初始化列表之前:
1234567 | class StrVec{public: StrVec(StrVec&&) noexcept;};StrVec::StrVec(StrVec&& s) noexcept : /*成员初始化器*/{/*构造函数体*/} |
必须在头文件的声明和定义中都指明noexcept
。
移动赋值运算符
移动赋值运算符执行与移动构造函数相同的工作:
1234567891011 | StrVec& StrVec::operator=(StrVec&& rhs) noexcept{ if(this!=&rhs){ free(); elements=rhs.elements; first_tree=rhs.first_tree; cap=rhs.cap; rhs.elements=rhs.first_tree=rhs.cap=nullptr; } return *this;} |
这里我们首先检查this
指针是否与rhs
的地址相同,如果不相同则先释放自身的资源。
移后原对象必须可销毁
我们编写一个移动操作后,必须确保移后源对象也就是被“窃取”的对象进入一个可析构的状态,我们的StrVec
实现这里这一点,我们将指针成员设置为nullptr
。
并且移动操作还必须保证对象仍然是有效的,也就是可以安全地对该对象赋新值,另外移动操作对对象的值没有任何要求。
合成的移动操作
与拷贝操作不同,编译器不会为某些类合成移动操作。特别的是,如果一个类定义了自己的拷贝构造函数,拷贝赋值运算符以及析构函数,那么 编译器不会为类合成移动构造函数和移动赋值运算符了。
只有当一个类没有定义任何自己版本的拷贝控制成员的时候,而且任何除static
的数据成员都可以移动时,编译器才会合成移动成员。编译器可以移动内置成员以及定义了移动操作的类类型成员:
1234567 | struct X{ int i; string s;};struct hasX{ X mem;} |
12 | X x,x2=std::move(x); //使用合成的构造函数hasX,hasX2=std::move(hasX); //使用合成的移动构造函数 |
移动操作永远不会被定义为删除的函数,但是我们如果显式地声明=default
,而编译器不能移动所有的成员,那么将会把移动操作定义为删除的。什么时候将移动操作定义为删除与拷贝成员的原则类似:
假如我们有一个类Y
,它定义了自己的拷贝构造函数而未定义自己的移动构造函数:
123456 | struct hasY{ hasY()=default; hasY(hasY&&)=defualt; Y mem;}hasY y,y2=std::move(y); |
这里不能进行移动操作,因为Y
成员没有定义自己的移动操作,那么按照hasY
的default
版本去移动Y
成员,而Y
这里没有移动构造函数,并且编译器也不会为Y
定义合成的移动操作,因为Y
已经定义了拷贝成员。
移动右值,拷贝左值
编译器使用普通的函数匹配规则来确定使用赋值构造函数还是移动构造函数,移动构造函数只能接受右值参数:
12 | StrVec getVec(istream&);v2=getVec(cin); |
在这里由于getVec
函数返回的是右值,所以对v2
使用移动构造函数。
另外一种情况是如果没有移动构造函数,即使是右值也会被拷贝。
123456 | class {public: Foo()=default; Foo(const Foo&); };Foo x,x1=std::move(x); |
这里即使x
是右值,但是由于没有移动构造函数,所以仍然会执行拷贝,因为这里我们可以将一个Foo&&
转换为const Foo&
类型的对象。
拷贝并交换赋值运算符和移动操作
123456 | class HasPtr{public: HasPtr(HasPtr&& p) noexcept:ps(p.ps),i(p.i) {p.ps=0;} HasPtr operator=(HasPtr rhs) (swap(*this,rhs);return *this;)} |
现在我们来观察赋值运算符,赋值运算符有一个非引用参数,这意味着此参数要进行拷贝初始化,对于实参的类型拷贝初始化又可以调用拷贝构造函数或者已经定义的移动构造函数–左值被拷贝,右值被移动。这样单一的赋值运算符就可以实现两种运算符的功能。
123 | HasPtr p,p2;p=p2;p=std::move(p2); |
在第一个赋值中调用的是拷贝赋值运算符,而第二个是移动赋值运算符。
Message类的移动操作
通过定义移动操作,我们可以使用string
和set
的移动操作来避免拷贝contents
和folders
成员的额外开销。同时我们必须更新每个拥有原Message
的folder,所以需要删除指向原Message
的旧指针,更新为新的指针。
我们定义一个函数来完成这一工作:
123456789 | void Message::move_Folders(Message* m){ folders=std::move(m->folders);//进行移动操作 for(auto f:folders){ f->remMsg(m); f->addMsg(this); } m->folders.clear();} |
我们这里使用的folders
这个set
的移动操作。
同时我们定义了Message
的移动构造函数来移动contents
,并且初始化自己的folders
。
1234 | Message::Message(Message&& m):contents(std::move(m.contents)){ move_Folders(&m);} |
移动赋值运算符检查自赋值情况:
123456789 | Message& Message::operator=(Message&& rhs){ if(this!=rhs){ remove_from_Folders(this);//从现有的folders中删除旧Message contents=std::move(rhs.contents); move_Folders(&rhs); } return *this;} |
移动迭代器
这部分内容要参考前面StrVec
的定义,这里不再详述。
右值引用和成员函数
除了拷贝与移动构造函数和运算符之外,成员函数如果也能提供拷贝和移动版本,那无疑是最好的。比如push_back
就提供两个版本:
12 | void push_back(const X&);void push_back(X&&); |
当我们传递一个可修改的右值也就是非const
时,编译器会匹配第二个版本。不定义一个接受const X&&
的原因是明显的,我们要从X
中窃取资源。
右值和左值引用成员函数
我们可以在右值对象上调用成员函数:
12 | string s1="ano",s2="ther";auto n=(s1+s2).find('a'); |
甚至还能够赋值:
1 | s1+s2="value"; |
新标准仍然支持这种向右值赋值,但是我们希望在类中阻止这种做法,希望左侧运算对象(即this
指向的对象)是一个左值,我们会在成员函数的参数列表后添加一个引用限定符:
123456789 | class {public: Foo& operator=(const Foo&) &;//只能向可修改的左值赋值};Foo& operator=(const Foo& rhs) &{ //... return *this;} |
引用限定符可以是&
或者&&
,分别指示在使用该成员时this
可以指向左值或者右值。
12345 | Foo& getFoo();Foo retFoo();Foo i,j;getFoo=i;retFoo=j;//错误,retFoo返回左值,不能使用赋值运算符 |
可以同时使用const
和&
限定:
1 | Foo& operator=(const Foo&) const &; |
重载和引用函数
使用引用限定符也可以区分重载版本:
123456789101112131415161718 | class {public: Foo sorted() &&; Foo sorted() const &;private: vector<string> data;};Foo sorted() &&{ sort(data.begin(),data.end()); return *this;}Foo sorted() const &{ Foo ret(*this); sort(ret.data.begin(),ret.data.end()); return ret;} |
对象是右值时,意味着该对象没有其他用户,这时可以改变对象。当对一个const
右值或者一个左值执行sorted
时候,我们不能改变对象,所以需要拷贝对象。
我们可以定义两个版本的成员,一个有const
,另一个没有。引用限定则不一样,对于返回类型,函数名和参数列表来讲,必须对所有的版本加上引用限定符,或者都不加:
12345678 | class Foo{public: Foo sorted() &&; Foo sorted() const;//错误,必须加上引用限定符 using Comp=bool(const int&,const int&);//Comp为函数类型的别名,此函数用来比较int值 Foo sorted(Comp*); Foo sorted(Comp*) const;//正确,都没有引用限定符} |