Part III: Tools for Class Authors
Chapter 13. Copy Control
13.4 复制控制示例
资源管理并不是类需要定义复制控制成员的唯一原因。一些类具有簿记或复制控制成员必须执行的其他动作。
下面概述可使用在邮件处理应用程序的两个类,作为需要复制控制来完成簿记操作的类的示例。
两个类 Message 和 Folder 分别表示电子邮件(或其他类型的)消息和消息出现的目录。
每个 Message 可以出现在多个 Folder 中。然而,任意给定的 Message 的内容只有一个副本。这样,如果一个 Message 的内容改变了,当在它所在的任何 Folder 中查看此 Message 时,都可看到改变后的内容。
为了记录 Message 位于哪些 Folder 中,每个 Message 会保存一个 set,包含指向该 Message 所在 Folder 的指针。
Message 类提供 save 和 remove 操作,在一个指定的 Folder 增加或删除一个 Message。
创建一个新 Message 时,会指定消息的内容,不会指定 Folder。为了将 Message 放入特定的 Folder 中,必须调用 save。
当复制一个 Message 时,副本和原对象是不同的 Message,但两种 Message 都出现在相同 set 的 Folder 中。因此,复制 Message 将复制内容和 Folder 指针的 set。对于这些 Folder,还必须增加指向新创建的 Message 的指针。
当销毁一个 Message 时,这个 Message 不再存在。因此,销毁 Message 必须从包含该 Message 的 Folder 中删除指向该 Message 的指针。
当将一个 Message 赋值给另一个时,将左侧 Message 的 contents 替换成右侧的。必须更新 Folder 的 set,从先前的 Folder 中删除左侧的 Message,将 Message 增加到右侧 Message 出现的 Folder 中。
实践:复制赋值运算符执行的工作,通常与复制构造函数和析构函数需要的工作一样。这种情况下,公共的工作应该放在 private 工具函数中。
Folder 类需要类似的复制控制成员,在它存储的 Message 中增加或删除它本身。
假定 Folder 类有名为 addMsg 和 remMsg 成员,分别在给定 Folder 的消息集合中增加或删除这个 Message。
Message 类
class Message {
friend class Folder;
public:
// folders is implicitly initialized to the empty set
explicit Message(const std::string &str = ""): contents(str) { }
// copy control to manage pointers to this Message
Message(const Message&); // copy constructor
Message& operator=(const Message&); // copy assignment
~Message(); // destructor
// add/remove this Message from the specified Folder's set of messages
void save(Folder&);
void remove(Folder&);
private:
std::string contents; // actual message text
std::set<Folder*> folders; // Folders that have this Message
// utility functions used by copy constructor, assignment, and destructor
// add this Message to the Folders that point to the parameter
void add_to_Folders(const Message&);
// remove this Message from every Folder in folders
void remove_from_Folders();
};
save 和 remove 成员
void Message::save(Folder &f) {
folders.insert(&f); // add the given Folder to our list of Folders
f.addMsg(this); // add this Message to f's set of Messages
}
void Message::remove(Folder &f) {
folders.erase(&f); // take the given Folder out of our list of Folders
f.remMsg(this); // remove this Message to f's set of Messages
}
Message 类的复制控制
// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m) {
for (auto f : m.folders) // for each Folder that holds m
f->addMsg(this); // add a pointer to this Message to that Folder
}
Message 复制构造函数复制给定对象的复制成员:
Message::Message(const Message &m): contents(m.contents), folders(m.folders) {
add_to_Folders(m); // add this Message to the Folders that point to m
}
Message 析构函数
// remove this Message from the corresponding Folders
void Message::remove_from_Folders() {
for (auto f : folders) // for each pointer in folders
f->remMsg(this); // remove this Message from that Folder
}
Message::~Message() {
remove_from_Folders();
}
编译器自动调用 string 析构函数释放 contents,调用 set 析构函数清除这些成员使用的内存。
Message 复制赋值运算符
Message& Message::operator=(const Message &rhs) {
// handle self-assignment by removing pointers before inserting them
remove_from_Folders(); // update existing Folders
contents = rhs.contents; // copy message contents from rhs
folders = rhs.folders; // copy Folder pointers from rhs
add_to_Folders(rhs); // add this Message to those Folders
return *this;
}
Message 的 swap 函数
void swap(Message &lhs, Message &rhs) {
using std::swap; // not strictly needed in this case, but good habit
// remove pointers to each Message from their (original) respective Folders
for (auto f: lhs.folders)
f->remMsg(&lhs);
for (auto f: rhs.folders)
f->remMsg(&rhs);
// swap the contents and Folder pointer sets
swap(lhs.folders, rhs.folders); // uses swap(set&, set&)
swap(lhs.contents, rhs.contents); // swap(string&, string&)
// add pointers to each Message to their (new) respective Folders
for (auto f: lhs.folders)
f->addMsg(&lhs);
for (auto f: rhs.folders)
f->addMsg(&rhs);
}
13.5 管理动态内存的类
有些类需要在运行时间分配可变大小的内存空间。这种类通常可以使用库容器存储数据。
然而,这种策略并不适用每个类;有些类需要自己完成分配。这种类一般必须定义自己的复制控制成员来管理它们分配的内存。
例如,实现库 vector 类的一个简化版本 StrVec,该类只存储 string。
StrVec 类的设计
vector 类在连续存储空间中存储它的元素。为了获得可接受的性能,vector 预先分配足够的空间来存储比可能需要的更多的元素。每个添加元素的 vector 成员会检查是否有可用的空间容纳更多的元素。如果有,成员在下一个可用空间构造一个对象。如果没有,vector 重新分配:vector 获取新空间,将已存在的元素存储到新空间中,释放旧空间,添加新元素。
在 StrVec 类中使用相似的策略。使用一个 allocator 获得原始内存(第12章)。因为 allocator 分配的内存是未构造的,当添加元素时,使用 allocator 的 construct 成员在那个空间创建对象。当删除元素时,使用 destroy 成员销毁元素。
每个 StrVec 有 3 个指针指向其元素所使用的内存:
- elements,指向分配内存的第一个元素
- first_free,指向最后实际元素的后面位置
- cap,指向分配内存末尾之后的位置
除了这些指针,StrVec 还有一个名为 alloc 的成员,类型是 allocator<string>。alloc 分配 StrVec 使用的内存。
StrVec 类还有 4 个工具函数:
- alloc_n_copy,分配空间,复制给定范围内的元素。
- free,销毁构造的元素,释放空间。
- chk_n_alloc,确保 StrVec 至少有容纳一个新元素的空间。如果没有,调用 reallocate 获取更多空间。
- reallocate,空间用完时重新分配 StrVec。
StrVec 类的定义
// simplified implementation of the memory allocation strategy for a vector-like class
class StrVec {
public:
StrVec(): // the allocator member is default initialized
elements(nullptr), first_free(nullptr), cap(nullptr) { }
StrVec(const StrVec&); // copy constructor
StrVec &operator=(const StrVec&); // copy assignment
~StrVec(); // destructor
void push_back(const std::string&); // copy the element
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
// ...
private:
std::allocator<std::string> alloc; // allocates the elements
// used by the functions that add elements to the StrVec
void chk_n_alloc() { if (size() == capacity()) reallocate(); }
// utilities used by the copy constructor, assignment operator, and destructor
std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
void free(); // destroy the elements and free the space
void reallocate(); // get more space and copy the existing elements
std::string *elements; // pointer to the first element in the array
std::string *first_free; // pointer to the first free element in the array
std::string *cap; // pointer to one past the end of the array
};
使用 construct
void StrVec::push_back(const string& s) {
chk_n_alloc(); // ensure that there is room for another element
// construct a copy of s in the element to which first_free points
alloc.construct(first_free++, s);
}
alloc_n_copy 成员
pair<string*, string*> StrVec::alloc_n_copy(const string *b, const string *e) {
// allocate space to hold as many elements as are in the range
auto data = alloc.allocate(e - b);
// initialize and return a pair constructed from data and the value returned by uninitialized_copy
return {data, uninitialized_copy(b, e, data)};
}
free 成员
void StrVec::free() {
// may not pass deallocate a 0 pointer; if elements is 0, there's no work to do
if (elements) {
// destroy the old elements in reverse order
for (auto p = first_free; p != elements; /* empty */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
复制控制成员
StrVec::StrVec(const StrVec &s) {
// call alloc_n_copy to allocate exactly as many elements as in s
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() { free(); }
StrVec &StrVec::operator=(const StrVec &rhs) {
// call alloc_n_copy to allocate exactly as many elements as in rhs
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
在重新分配过程中移动而不是复制元素
reallocate 成员函数应该做:
- 为一个新的、更大的 string 数组分配内存
- 构造空间的前一部分,保存现存的元素
- 销毁现存内存的元素,释放内存
因为 string 的行为类似值,复制 string 必须为这些字符分配内存,销毁 string 必须释放该 string 使用的内存。
复制 string 中的数据是不必要的。在每次重新分配时,如果可以避免分配和释放 string 的开销,StrVec 的性能会好很多。
移动构造函数和 std::move
通过使用C++11标准库引入的两种机制,来避免复制字符串。
-
首先,几个库类,包括 string,定义了所谓的“移动构造函数”。 移动构造函数通常通过将资源从给定对象“移动”到正在构造的对象来进行操作。标准库保证“移动源”string 保持在有效,可析构的状态。
-
第二种机制是名为
move
的库函数,它定义在 utility 头文件中。
①当 reallocate 在新内存中构造 string 时,它必须调用 move 表示要使用 string 移动构造函数。如果忽略调用 move,会使用 string 的复制构造函数。
②通常不会为 move 提供 using 声明。当使用 move 时,调用 std::move,而不是 move。(第18章)
reallocate 成员
void StrVec::reallocate() {
// we'll allocate space for twice as many elements as the current size
auto newcapacity = size() ? 2 * size() : 1;
// allocate new memory
auto newdata = alloc.allocate(newcapacity);
// move the data from the old memory to the new
auto dest = newdata; // points to the next free position in the new array
auto elem = elements; // points to the next element in the old array
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); // free the old space once we've moved the elements
// update our data structure to point to the new elements
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
13.6 移动对象
C++11标准的一个主要特性是可以移动而非复制对象的能力。
在某些情况下,对象复制后会被立即销毁。这种情况下,移动而非复制对象可大幅度提升性能。
在类中使用移动而非复制的另一个原因是源于IO类或 unique_ptr 类。这些类包含不能被共享的资源,如指针或IO缓冲。
笔记:库容器、string、shared_ptr 类既支持移动,也支持复制。IO类和 unique_ptr 类可以移动,但不能复制。
右值引用
为了支持移动操作,C++11标准引入了一种新的引用类型——右值引用 (rvalue reference)。右值引用是必须被绑定到右值的引用。右值引用通过 &&
来获得。
一般而言,左值表达式指的是对象的身份,右值表达式指的是对象的值。
为了与右值引用进行区分,将常规引用称为左值引用 (lvalue reference)。不能将其绑定到要求转换、常量、或返回右值的表达式。右值引用有相反的绑定属性:可以将右值引用绑定到这类表达式,但不能直接将右值引用绑定到左值。
int i = 42;
int &r = i; // ok: r refers to i
int &&rr = i; // error: cannot bind an rvalue reference to an lvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue
int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication
返回左值引用的函数,赋值、下标、解引用、前置递增/递减运算符,都是返回左值表达式的例子。可以将左值引用绑定到这些表达式的结果上。
返回非引用类型的函数,算术、关系、位、后置递增/递减运算符,都生成右值。不能将左值引用绑定到这些表达式,但可以将右值引用或 const 的左值引用绑定到这些表达式。
左值持续存在;右值是短暂的
左值和右值的区别:左值有持续的状态,右值是常量或表达式求值过程中创建的临时对象。
因为右值引用只能绑定到临时对象,得知:
- 所引用的对象将要被销毁
- 该对象没有其他的使用者
这些事实意味着:使用右值引用的代码可以自由地从引用指向的对象中接管资源。
变量是左值
int &&rr1 = 42; // ok: literals are rvalues
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!
不能直接将右值引用绑定到一个变量,即使该变量定义成右值引用类型。
库 move 函数
虽然不能直接将右值引用绑定到左值上, 但是可以将一个左值显示转换成其对应的右值引用类型。通过调用 move
函数,可以获取绑定到左值的右值引用。
int &&rr3 = std::move(rr1); // ok
调用 move 承诺:不会再次使用 rr1,除非向它赋值或销毁它。
移动构造函数和移动赋值运算符
为了让自己编写的类型能够进行移动操作,定义一个移动构造函数和一个移动赋值运算符。
与复制构造函数类似,移动构造函数有一个初始参数,该参数是对类类型的引用;任何额外的形参必须有默认实参。
与复制构造函数不同,移动构造函数中的引用形参是一个右值引用。
除了移动资源之外,移动构造函数必须确保移动源对象处于这样一个状态:销毁它是无害的。特别地,一旦资源被移动,原对象必须不再指向被移动的资源——这些资源已经由新创建的对象负责。
StrVec::StrVec(StrVec &&s) noexcept // move won't throw any exceptions
// member initializers take over the resources in s
: elements(s.elements), first_free(s.first_free), cap(s.cap)
{
// leave s in a state in which it is safe to run the destructor
s.elements = s.first_free = s.cap = nullptr;
}
移动构造函数不会分配任何新的内存;它接管给定 StrVec 给定的内存。
从实参中接管内存后,构造函数体将给定对象的指针设置为 nullptr。对象被移动后,这个对象继续存在。
最终,移动源对象被销毁,意味着将在这个对象上运行析构函数。
StrVec 的析构函数在 first_free 调用 deallocate,如果忘记改变 s.first_free,那么销毁移动源对象会删除刚才移动的内存。
移动操作、库容器和异常
因为移动操作通过“窃取”资源来执行,它通常不分配任何资源。因此,移动操作通常不会抛出异常。
当编写一个不抛出异常的移动操作时,应该将这个事实通知库。
如果库不知道移动函数不会抛出异常,它会做一些额外的工作,来处理移动类类型的对象会抛出异常这种可能性。
通知库的一种方式是在构造函数中指定 noexcept
。noexcept 是C++11标准引入的(第18章)。
class StrVec {
public:
StrVec(StrVec&&) noexcept; // move constructor
// other members as before
};
StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */ { /* constructor body */ }
如果定义在类外,必须在类头文件的声明中和定义中都指定 noexcept。
不抛出异常的移动构造函数和移动赋值运算符应该被标记为 noexcept。
移动赋值运算符
移动赋值运算符做的工作与析构函数和赋值构造函数的工作一样。
StrVec &StrVec::operator=(StrVec &&rhs) noexcept {
// direct test for self-assignment
if (this != &rhs) {
free(); // free existing elements
elements = rhs.elements; // take over resources from rhs
first_free = rhs.first_free;
cap = rhs.cap;
// leave rhs in a destructible state
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
移动源对象必须可析构
从对象中移动不会销毁这个对象:在移动操作完成后的某个时间,移动源对象会被销毁。因此,在写移动操作时,必须保证移动源对象处在一个可析构的状态。StrVec 通过将移动源对象的成员设置为 nullptr 使移动操作满足这个要求。
注意:在移动操作后,移动源对象必须保持是一个有效的、可析构的对象,但使用者不能对它的值作任何假设。
合成的移动操作
对于某些类,编译器根本不会为它合成移动操作。特别地,如果类定义了自己的复制构造函数、复制赋值运算符、或析构函数,移动构造函数和移动赋值运算符不会被合成。因此,有些类没有移动构造函数和移动赋值运算符。
如果类没有移动操作,通过正常的函数匹配,其对应的复制操作会被用来替代移动。
只有当类没有定义任何自己的复制控制成员,且类的每个非static数据成员都可以移动,编译器才会合成移动构造函数或移动赋值运算符。
编译器可以移动内置类型的成员。如果成员是一个类类型,且该类有对应的移动操作,那么编译器也可以移动这个成员。
// the compiler will synthesize the move operations for X and hasX
struct X {
int i; // built-in types can be moved
std::string s; // string defines its own move operations
};
struct hasX {
X mem; // X has synthesized move operations
};
X x, x2 = std::move(x); // uses the synthesized move constructor
hasX hx, hx2 = std::move(hx); // uses the synthesized move constructor
与复制操作不同,移动操作从不会隐式地定义成删除的函数。但是,如果通过使用 = default
显示要求编译器生成一个移动操作,且编译器不能移动所有成员,那么移动操作会被定义成删除的函数。
何时合成的移动操作会被定义成删除的函数:
- 与复制构造函数不同,移动构造函数定义成删除的函数的条件:类有一个成员定义了自己的复制构造函数,但没有定义移动构造函数;或者类有一个成员没有定义自己的复制操作,且编译器不能为它合成移动构造函数。移动赋值运算符与该情况类似。
- 如果类有一个成员,它自己的移动构造函数或移动赋值运算符是删除的或不可访问的,那么类的移动构造函数或移动赋值运算符被定义成删除的。
- 与复制构造函数类似,如果析构函数是删除的或不可访问的,那么移动构造函数被定义成删除的。
- 与复制赋值运算符类似,如果类有一个 const 或引用成员,那么移动赋值运算符被定义成删除的。
// assume Y is a class that defines its own copy constructor but not a move constructor
struct hasY {
hasY() = default;
hasY(hasY&&) = default;
Y mem; // hasY will have a deleted move constructor
};
hasY hy, hy2 = std::move(hy); // error: move constructor is deleted
如果类定义一个移动构造函数和/或一个移动赋值运算符,那么该类的合成的复制构造函数和复制赋值运算符会被定义成删除的。
注意:定义了移动构造函数或移动赋值运算符的类必须定义它自己的复制操作。否则,这些成员会被默认是删除的。
移动右值,复制左值 …
当一个类同时有复制构造函数和移动构造函数,编译器使用普通的函数匹配来决定使用那个构造函数。赋值操作类似。
StrVec v1, v2;
v1 = v2; // v2 is an lvalue; copy assignment
StrVec getVec(istream &); // getVec returns an rvalue
v2 = getVec(cin); // getVec(cin) is an rvalue; move assignment
在第二个赋值中,两种赋值操作都是可行的——可以将 getVec 的结果绑定到任一个赋值操作的形参。调用复制赋值运算符需要将其转换为 const,而 StrVec&& 是精准匹配。因此,第二个赋值使用移动赋值运算符。
… 如果没有移动构造函数,那么右值会被复制
class Foo {
public:
Foo() = default;
Foo(const Foo&); // copy constructor
// other members, but Foo does not define a move constructor
};
Foo x;
Foo y(x); // copy constructor; x is an lvalue
Foo z(std::move(x)); // copy constructor, because there is no move constructor
“复制-交换”赋值运算符和移动
class HasPtr {
public:
// added move constructor
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;}
// assignment operator is both the move- and copy-assignment operator
HasPtr& operator=(HasPtr rhs) { swap(*this, rhs); return *this; }
// other members as in § 13.2
};
上面的赋值运算符有一个非引用形参,这意味着形参是复制初始化。根据实参的类型,复制初始化要么使用复制构造函数,要么使用移动构造函数;左值被复制,右值被移动。因此,这个赋值运算符同时充当复制赋值运算符和移动赋值运算符。
例如,假设 hp 和 hp2 是 HasPtr 对象:
hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2
hp = std::move(hp2); // move constructor moves hp2
建议:更新三/五法则
所有五个复制控制成员应该被视为一个整体:一般来说,如果一个类定义了这些操作的任何一个,它通常应该定义它们这些所有操作。
Message 类的移动操作
// move the Folder pointers from m to this Message
void Message::move_Folders(Message *m) {
folders = std::move(m->folders); // uses set move assignment
for (auto f : folders) { // for each Folder
f->remMsg(m); // remove the old Message from the Folder
f->addMsg(this); // add this Message to that Folder
}
m->folders.clear(); // ensure that destroying m is harmless
}
Message::Message(Message &&m): contents(std::move(m.contents)) {
move_Folders(&m); // moves folders and updates the Folder pointers
}
Message& Message::operator=(Message &&rhs) {
if (this != &rhs) { // direct check for self-assignment
remove_from_Folders();
contents = std::move(rhs.contents); // move assignment
move_Folders(&rhs); // reset the Folders to point to this Message
}
return *this;
}
注意:向 set 插入一个元素可能会抛出异常——向容器添加元素要求分配内存,意味着可能会抛出 bad_alloc 异常。因此,Message 移动构造函数和移动赋值运算符可能会抛出异常,没有将它们标记为 noexcept。
移动迭代器
C++11标准库定义了一种移动迭代器 (move iterator) 适配器。移动迭代器通过改变给定迭代器解引用运算符的行为,来适配此迭代器。一般来说,解引用运算符返回一个指向元素的左值引用。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。
通过调用库 make_move_iterator
函数,将一个普通迭代器转换为移动迭代器。这个函数接受一个迭代器,返回一个移动迭代器。
void StrVec::reallocate() {
// allocate space for twice as many elements as the current size
auto newcapacity = size() ? 2 * size() : 1;
auto first = alloc.allocate(newcapacity);
// move the elements
auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first);
free(); // free the old space
elements = first; // update the pointers
first_free = last;
cap = elements + newcapacity;
}
uninitialized_copy 在输入序列的每个元素上调用 construct 来“复制”元素到目的地址。这个算法使用迭代器解引用运算符从输入序列中提取元素。因为传递的是移动迭代器,所以解引用运算符生成右值引用,意味着 construct 使用移动构造函数来构造元素。
建议:不要轻易使用移动操作
由于移动源对象的状态不确定,因此在对象上调用 std::move 是危险的操作。当调用 move 时,必须绝对确定,移动源对象没有其他用户。
除了诸如移动构造函数或移动赋值运算符这些类实现代码之外,仅当确定需要进行移动并且保证移动是安全的,才可使用 std::move。
右值引用和成员函数
除了构造函数和赋值运算符之外,其他成员函数也可提供复制和移动两个版本。
这些能够移动的成员一般使用与复制/赋值构造函数和赋值运算符相同的形参模式——一个版本接受一个指向 const 的左值引用,第二个版本接受一个指向非const 的右值引用。
void push_back(const X&); // copy: binds to any kind of X
void push_back(X&&); // move: binds only to modifiable rvalues of type X
区分复制和移动形参的重载函数:一个版本接受 const T&
,一个接受 T&&
。
class StrVec {
public:
void push_back(const std::string&); // copy the element
void push_back(std::string&&); // move the element
// other members as before
};
// unchanged from the original version in § 13.5
void StrVec::push_back(const string& s) {
chk_n_alloc(); // ensure that there is room for another element
// construct a copy of s in the element to which first_free points
alloc.construct(first_free++, s);
}
void StrVec::push_back(string &&s) {
chk_n_alloc(); // reallocates the StrVec if necessary
alloc.construct(first_free++, std::move(s));
}
StrVec vec; // empty StrVec
string s = "some string or another";
vec.push_back(s); // calls push_back(const string&)
vec.push_back("done"); // calls push_back(string&&)
右值和左值引用成员函数
通常,可以在对象上调用成员函数,不管这个对象是左值还是右值:
string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a'); // call the find member on the string rvalue that results from adding two strings.
s1 + s2 = "wow!"; // assign to the rvalue result of concatentating these strings.
C++11之前的标准中,无法阻止上述的用法。为了保持向后兼容性,C++11标准库类仍然允许分配给右值,但是,我们可能希望在自己的类中防止这种用法。在这种情况下,我们希望将左侧操作数(即 this 指向的对象)强制是一个左值。
我们指出 this 的左值/右值属性的方式与定义 const 成员函数相同;在形参列表后放置一个引用限定符 (reference qualifier)。
class Foo {
public:
Foo &operator=(const Foo&) &; // may assign only to modifiable lvalues
// other members of Foo
};
Foo &Foo::operator=(const Foo &rhs) & {
// do whatever is needed to assign rhs to this object
return *this;
}
引用限定符可以是 &
或 &&
,分别指出 this 可指向左值或右值。
引用限定符只能出现在非static成员函数中,且必须同时出现在函数的声明和定义中。
Foo &retFoo(); // returns a reference; a call to retFoo is an lvalue
Foo retVal(); // returns by value; a call to retVal is an rvalue
Foo i, j; // i and j are lvalues
i = j; // ok: i is an lvalue
retFoo() = j; // ok: retFoo() returns an lvalue
retVal() = j; // error: retVal() returns an rvalue
i = retVal(); // ok: we can pass an rvalue as the right-hand operand to assignment
一个函数可以同时是 const 和引用限定。这种情况下,引用限定符必须在 const 限定符后面。
class Foo {
public:
Foo someMem() & const; // error: const qualifier must come first
Foo anotherMem() const &; // ok: const qualifier comes first
};
重载和引用函数
class Foo {
public:
Foo sorted() &&; // may run on modifiable rvalues
Foo sorted() const &; // may run on any kind of Foo
// other members of Foo
private:
vector<int> data;
};
// this object is an rvalue, so we can sort in place
Foo Foo::sorted() && {
sort(data.begin(), data.end());
return *this;
}
// this object is either const or it is an lvalue; either way we can't sort in place
Foo Foo::sorted() const & {
Foo ret(*this); // make a copy
sort(ret.data.begin(), ret.data.end()); // sort the copy
return ret; // return the copy
}
retVal().sorted(); // retVal() is an rvalue, calls Foo::sorted() &&
retFoo().sorted(); // retFoo() is an lvalue, calls Foo::sorted() const &
如果一个成员函数有引用限定符,具有相同形参列表的这个成员的所有版本必须都有引用限定符。
class Foo {
public:
Foo sorted() &&;
Foo sorted() const; // error: must have reference qualifier
// Comp is type alias for the function type (see § 6.7 )
// that can be used to compare int values
using Comp = bool(const int&, const int&);
Foo sorted(Comp*); // ok: different parameter list
Foo sorted(Comp*) const; // ok: neither version is reference qualified
};