const
引用的拷贝构造函数,但此参数几乎总是一个const
的引用。当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
拷贝初始化通常使用拷贝构造函数来完成。但是,如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而非拷贝构造函数来完成。
拷贝构造函数在以下几种情况下会被使用(发生拷贝初始化):
如果我们希望使用一个explicit构造函数,就必须显式地使用。
vector<int> v1(10); // 正确:直接初始化
vector<int> v2 = 10; // 错误:接受大小参数的构造函数是explicit的
void f(vector<int>); // f的参数进行拷贝初始化
f(10); // 错误:不能用一个explicit的构造函数拷贝一个实参
f(vector<int>(10)); // 正确:从一个int直接构造一个临时vector
在拷贝初始化过程中,编译器可以(但不是必须)跳过拷贝/移动构造函数,直接创建对象。即,编译器被允许将下面的代码改写:
string null_book = "9-999-9999-9"; // 拷贝初始化
string null_book("9-999-99999-9"); // 编译器略过了拷贝构造函数
但是,即使编译器略过了拷贝/移动构造函数,但在这个程序点上,拷贝/移动构造函数必须是存在且可访问的(例如,不能是private的)。
重载运算符的参数表示运算符的运算对象。某些运算符,包括赋值运算符,必须定义为成员函数。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数。对于一个二元运算符,例如赋值运算符,其右侧运算对象作为显式参数传递。
拷贝赋值运算符接受一个与其所在类相同类型的参数:
class Foo
{
public:
Foo& operator=(const Foo&); // 赋值运算符
// ...
};
赋值运算符通常应该返回一个指向其左侧运算对象的引用。
与处理拷贝构造函数一样,如果一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。
类似拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值。如果拷贝赋值运算符并非出于此目的,它会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,这一工作是通过成员类型的拷贝赋值运算符来完成的。对于数组类型的成员,逐个赋值数组元素。合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
// 等价于合成拷贝赋值运算符
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo; // 调用string::operator=
units_sold = rhs.units_sold; // 使用内置的int赋值
revenue = rhs.revenue; // 使用内置的double赋值
return *this; // 返回一个此对象的引用
}
析构函数释放对象使用的资源,并销毁对象的非static数据成员。析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数:
class Foo
{
public:
~Foo(); // 析构函数
// ...
};
由于析构函数不接受参数,因此它不能被重载。对一个给定类,只会有唯一一个析构函数。
无论何时一个对象被销毁,就会自动调用其析构函数:
仍需要强调的是,当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
类似拷贝构造函数和拷贝赋值运算符,对于某些类,合成析构函数被用来阻止该类型的对象被销毁。如果不是这种情况,合成析构函数的函数体就为空。
// 下面的代码片段等价于Sales_data的合成析构函数
class Sales_data
{
public:
// 成员会被自动销毁,除此之外不需要做其他事情
~Sales_data() { }
// 其他成员的定义,如前
};
析构函数体自身并不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁过程中,析构函数体是作为成员销毁步骤之外的另一部分而进行的。
我们可以通过将拷贝控制成员定义为=default
来显式地要求编译器生成合成的版本。
当我们在类内用=default
修饰成员的声明时,合成的函数将隐式地声明为内联的(就像任何其他类内声明的成员函数一样)。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default
。
class Sales_data
{
public:
// 拷贝控制成员;使用default
Sales_data() = default;
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
(即,默认构造函数或拷贝控制成员)。
我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝:我们虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上=delete
来指出我们希望将它定义为删除的:
struct NoCopy
{
NoCopy() = default; // 使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; // 阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; // 阻止赋值
~NoCopy() = default; // 使用合成的析构函数
// 其他成员
};
与=default
不同,=delete
必须出现在函数第一次声明的时候,这个差异与这些声明的含义在逻辑上是吻合的。一个默认的成员只影响为这个成员而生成的代码,因此=default
直到编译器生成代码时才需要。而另一方面,编译器需要知道一个函数是删除的,以便禁止试图使用它的操作。
与=default
的另一个不同之处是,我们可以对任何函数指定=delete
(我们只能对编译器可以合成的默认构造函数或拷贝控制成员使用=default
)。虽然删除函数的主要用途是禁止拷贝控制成员,但当我们希望引导函数匹配过程时,删除函数有时也是有用的。
对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类的临时对象。而且,如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。
对于删除了析构函数的类型,虽然我们不能定义这种类型的变量或成员,但可以动态分配这种类型的对象。但是,不能释放这些对象:
struct NoDtor
{
NoDtor() = default; // 使用合成默认构造函数
~NoDtor() = delete; // 我们不能销毁NoDtor类型的对象
};
NoDtor nd; // 错误:NoDtor的析构函数是删除的
NoDtor *p = new NoDtor(); // 正确:但我们不能delete p
delete p; // 错误:NoDtor的析构函数是删除的
综上,对于析构函数已删除的类型,不能定义该类型的变量或释放指向该类型动态分配对象的指针。
const
的或引用成员,则类的合成拷贝赋值运算符被定义为删除的。const
成员,它没有类内初始化器且其类型未显式定义默认构造函数,则该类的默认构造函数被定义为删除的。因为试图访问一个未定义的成员会导致一个链接时错误,通过声明但不定义private
的拷贝构造函数,我们可以预先阻止任何拷贝该类型对象的企图:试图拷贝对象的用户代码将在编译阶段被标记为错误;成员函数或友元函数中的拷贝操作将会导致链接时错误。
// 在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝
class PrivateCopy{
// 无访问说明符;接下来的成员默认为private的
// 拷贝控制成员是private的,因此普通用户代码无法访问
PrivateCopy(const PrivateCopy &);
PrivateCopy &operator=(const PrivateCopy &);
// 其他成员
public:
PrivateCopy() = default; // 使用合成的默认构造函数
~PrivateCopy(); // 用户可以定义此类型的对象,但无法拷贝他们
};
希望阻止拷贝的类应该使用=delete
来定义它们自己的拷贝构造函数和拷贝赋值运算符,而不应该将它们声明为private
的。
通常,管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,我们首先必须确定此类型对象的拷贝语义。一般来说,有两种选择:可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针。
关键概念:赋值运算符
当你编写赋值运算符时,有两点需要记住:
当你编写一个赋值运算符时,一个好的模式是先将右侧运算对象拷贝到一个局部临时对象中。当拷贝完成后,销毁左侧运算对象的现有成员就是安全的了。一旦左侧运算对象的资源被销毁,就只剩下将数据从临时对象拷贝到左侧运算对象的成员中了。
// 这样编写赋值运算符是错误的
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
delete ps; // 释放对象指向的string
// 如果rhs和*this是同一个对象,我们就将从已释放的内存中拷贝数据!
ps = new string(*(rhs.ps));
i = rhs.i;
return *this;
}
// 正确处理自赋值的情况
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
auto newp = new string(*rhs.ps); // 拷贝底层string
delete ps; // 释放旧内存
ps = newp; // 从右侧运算对象拷贝数据到本对象
i = rhs.i;
return *this; // 返回本对象
}
令一个类展现类似指针的行为的最好方法是使用shared_ptr
来管理类中的资源。但是,有时我们希望直接管理资源。在这种情况下,使用引用计数就很有用了。引用计数的工作方式如下:
实现共享计数器的一种方法是将计数器保存在动态内存中。
class HasPtr
{
public:
// 构造函数分配新的string和新的计数器,将计数器置为1
HasPtr(const string &s = 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; // 用来记录有多少个对象共享*ps的成员
};
HasPtr &HasPtr::operator=(const HasPtr &rhs)
{
++*rhs.use; // 递增右侧运算对象的引用计数
if (--*use == 0) // 然后递减本对象的引用计数
{
delete ps; // 如果没有其他用户
delete use; // 释放本对象分配的成员
}
ps = rhs.ps; // 将数据从rhs拷贝到本对象
i = rhs.i;
use = rhs.use;
return *this; // 返回本对象
}
// 拷贝赋值操作实际上是拷贝构造操作与析构操作的复合,从上述代码中可以得到印证
HasPtr::~HasPtr()
{
if (--*use == 0) // 如果引用计数变为0
{
delete ps; // 释放string内存
delete use; // 释放计数器内存
}
}
除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。对于那些与重排元素顺序的算法一起使用的类,定义swap是非常重要的。这类算法在需要交换两个元素时会调用swap。
如果一个类定义了自己的swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的swap。
class HasPtr
{
friend void swap(HasPtr&, HasPtr&);
// 其他成员定义
};
// 由于swap的存在就是为了优化代码,我们将其声明为inline函数。
inline void swap(HasPtr &lhs, HasPtr &rhs)
{
using std::swap;
swap(lhs.ps, rhs.ps); // 交换指针,而不是string数据
swap(lhs.i, rhs.i); // 交换int成员
}
与拷贝控制成员不同,swap并不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。
内置类型是没有特定(自定义)版本的swap的,对swap的调用会调用标准库std::swap
。但是,如果一个类的成员有自己类型特定(自定义)的swap函数,调用std::swap
就是错误的了。
// 错误使用标准库的swap的示例:
void swap(Foo &lhs, Foo &rhs)
{
// 错误:这个函数使用了标准库版本的swap,而不是HasPtr版本
std::swap(lhs.h, rhs.h);
// 交换类型Foo的其他成员
};
// 正确示例:
void swap(Foo &lhs, Foo &rhs)
{
using std::swap;
swap(lhs.h, rhs.h); // 使用HasPtr版本的swap
// HasPtr的特定版本swap优于std::swap,匹配HasPtr版本的swap
// 交换类型Foo的其他成员
// ...
}
如果存在类型特定的swap版本,swap调用会与之匹配。如果不存在类型特定的版本,则会使用std
中的版本(假定作用域中有using
声明)。
定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:
// 注意rhs是按值传递的,意味着HasPtr的拷贝构造函数...
// ...将右侧运算对象中的string拷贝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
// 交换左侧运算对象和局部变量rhs的内容
swap(*this, rhs); // rhs现在指向本对象曾经使用的内存
return *this; // rhs被销毁,从而delete了rhs中的指针
}
使用拷贝并交换的赋值运算符自动就是异常安全的,且能正确处理自赋值。
string
和shared_ptr
类既支持移动也支持拷贝。IO类和unique_ptr
类可以移动但不能拷贝。右值引用——即必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
对于常规引用(我们可以称之为左值引用),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用可以绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上
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
的标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility
中。
int &&rr3 = std::move(rr1); // ok
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
使用move的代码应该使用std::move
而不是move
。这样做可以避免潜在的名字冲突。
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。
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;
}
与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管给定的对象中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr
。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行析构函数。
由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的工作。
一种通知标准库的方法是在我们的构造函数中指明noexcept
。在一个构造函数中,noexcept
出现在参数列表和初始化列表开始的冒号之间:
class StrVec
{
public:
StrVec(StrVec&&) noexcept; // 移动构造函数
// 其他成员的定义,如前
};
StrVec::StrVec(StrVec &&s) noexcept : /* 成员初始化器 */
{
/* 构造函数体 */
}
我们必须在类头文件的声明中和定义中(如果定义在类外的话)都指定noexcept
。
不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept
。
标记noexcept
的原因:首先,虽然移动操作通常不抛出异常,但抛出异常也是允许的;其次,标准库容器能对异常发生时其自身的行为提供保障。例如,vector保证,如果我们调用push_back时发生异常,vector自身不会发生改变。除非vector知道元素类型的移动构造函数不会抛出异常,否则在重新分配内存的过程中,它就必须使用拷贝构造函数(如果拷贝发生异常,vector可以释放新分配的内存并返回,vector原有的元素仍然存在)而不是移动构造函数(如果移动发生异常,旧空间中的移动源元素已经被改变了,而新空间中未构造的元素可能尚不存在)。如果希望在vector重新分配内存这类情况下对我们自定义类型的对象进行移动而不是拷贝,就必须显式地告诉标准库我们的移动构造函数可以安全使用。我们通过将移动构造函数(及移动赋值运算符)标记为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;
}
如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作,通过正常的函数匹配,类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有(非static)数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
与拷贝操作不同,移动操作永远不会隐式定义为删除的函数。但是,如果我们显式地要求编译器生成=default
的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。
除了一个重要例外,什么时候将合成的移动操作定义为删除的函数遵循与定义删除的合成拷贝操作类似的原则:
const
的或是引用,则类的移动赋值运算符被定义为删除的。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。因此,定义了一个移动构造函数或移动赋值运算符的类必须也定义自己的拷贝操作。否则,这些成员默认地被定义为删除的。
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。
StrVec v1, v2;
v1 = v2; // v2是左值;使用拷贝赋值
StrVec getVec(istream &); // getVec返回一个右值
v2 = getVec(cin); // getVec(cin)是一个右值;使用移动赋值
如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它们时也是如此:
class Foo
{
public:
Foo() = default;
Foo(const Foo&); // 拷贝构造函数
// 其他成员定义,但Foo未定义移动构造函数
};
Foo x;
Foo y(x); // 拷贝构造函数;x是一个左值
Foo z(std::move(x)); // 拷贝构造函数,因为未定义移动构造函数
总结为:移动右值,拷贝左值。但如果没有移动构造函数,右值也被拷贝。拷贝赋值运算符和移动赋值运算符的情况类似
拷贝并交换赋值运算符和移动操作:依赖于实参的类型,rhs
的拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能:
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; }
// 其他成员的定义
};
移动迭代器适配器:一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。而移动迭代器的解引用运算符生成一个右值引用。
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时,必须绝对确认移后源对象没有其他用户。std::move
。允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式——一个版本接受一个指向const
的左值引用,第二个版本接受一个指向非const
的右值引用。
// 假定X是元素类型
void push_back(const X&); // 拷贝:绑定到任意类型的X
void push_back(X&&); // 移动:只能绑定到类型X的可修改的右值
一般来说,我们不需要为函数操作定义接受一个const X&&
或是一个(普通的)X&
参数的版本。当我们希望从实参“窃取”数据时(要修改源对象),通常传递一个右值引用。为了达到这一目的,实参不能是const
的。类似的,从一个对象进行拷贝的操作不应该改变该对象。因此,通常不需要定义一个接受一个(普通的)X&
参数的版本。
简之,区分移动和拷贝的重载函数通常有一个版本接受一个const T&
,而另一个版本接受一个T&&
。
通常,我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值。
string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a');
s1 + s2 = "wow!";
在参数列表后放置一个引用限定符(&或&&)来指出this的左值/右值属性。类似const
限定符,引用限定符只能用于(非static)成员函数(修饰this指针),且必须同时出现在函数的声明和定义中(this指针是非static成员函数的参数之一,故声明和定义中参数this指针类型要求一致)。
class Foo
{
public:
Foo &operator=(const Foo&) &; // 只能向可修改的左值赋值
// Foo的其他参数
};
Foo &Foo::operator=(const Foo &rhs) &
{
// 执行将rhs赋予本对象所需的工作
return *this;
}
对于&限定的函数,我们只能将它用于左值;对于&&限定的函数,只能用于右值。
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限定符必须在前
Foo anotherMem() const &; // 正确:const限定符在前
};
引用限定符也可以区分重载版本。而且,我们可以综合引用限定符和const
来区分一个成员函数的重载版本。
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的对象的左值/右值属性来确定使用哪个sorted版本:
retVal().sorted(); // retVal()是一个右值,调用Foo::sorted() &&
retFoo().sorted(); // retFoo()是一个左值,调用Foo::sorted() const &
如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符(标注&还是&&)。
class Foo
{
public:
Foo sorted() &&;
Foo sorted() const; // 错误:必须加上引用限定符
// Comp是函数类型的类型别名
// 此函数类型可以用来比较int值
using Comp = bool(const int&, const int&);
Foo sorted(Comp*); // 正确:不同的参数列表
Foo sorted(Comp*) const; // 正确:两个版本都没有引用限定符
};