拷贝控制——拷贝控制和资源管理,交换操作,对象移动

一、拷贝控制和资源管理

   通常,管理类外资源的类必须定义拷贝控制成员,这种类需要通过析构函数来释放对象所分配的资源。

  为了定义这些成员,我们首先必须确定此类型对象的拷贝语义。一般来说,有两种选择:可以定义拷贝操作,使类的行为看起来像一个值或者像一个指针。

  类的行为像一个值,意味着它应该也有自己的状态。当我们拷贝一个像值的对象时,副本和原对象是完全独立的。改变副本不会对原对象有任何影响,反之亦然。

  行为像指针的类则共享状态。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本也会改变原对象,反之亦然。

  在我们使用的标准库类中,标准库容器和string类的行为像一个值,shared_ptr类提供类似指针的行为。

1、行为像值的类

  为了提供类值的行为,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。

1)类值拷贝赋值运算符

  赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源。类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。但是,非常重要的一点是,这些操作是以正确的顺序进行的,即使将一个对象赋予它自身,也保证正确。而且,如果可能,我们编写的赋值运算符还应该是异常安全的——当异常发生时能将左侧运算对象置于一个有意义的状态。

  注意:对于一个赋值运算符来说,正确工作是非常重要的,即使是将一个对象赋予它自身,也要能正确工作。一个好的方法是在销毁左侧运算对象资源之前拷贝右侧运算对象。

 1 #include <iostream>
 2 #include <string>
 3 
 4 class Demo
 5 {
 6 public:
 7     Demo(const std::string &s = std::string()) :
 8         ps(new std::string(s)), data(0) {}
 9     Demo(const Demo &rhs) : // 每个字符串都有自己的拷贝
10         ps(new std::string(*rhs.ps)), data(rhs.data) {}
11     Demo &operator=(const Demo &rhs)
12     {
13         auto newp = new std::string(*rhs.ps); // 拷贝底层string
14         delete ps; // 释放旧内存
15         ps = newp; // 从右侧运算对象拷贝数据到本对象
16         data = rhs.data;
17         return *this;
18     }
19     ~Demo() {
20         delete ps;
21     }
22     std::string *ps;
23     int data;
24 };
25 
26 int main()
27 {
28     Demo a, b;
29     *b.ps = "bb";
30     a = b;
31     std::cout << *a.ps << "  " << *b.ps << std::endl;
32     *b.ps = "cc";
33     std::cout << *a.ps << "  " << *b.ps << std::endl;
34     return 0;
35 }
View Code

 在本例中,通过先拷贝右侧运算对象,我们可以处理自赋值情况,并能保证在异常发生时代码也是安全的。

2、行为像指针的类

  对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它指向的string。我们的类仍需要自己的析构函数来释放接受string参数的构造函数分配的内存。但是,在本例中,析构函数不能单方面地释放关联的string。只有当最后一个指向string的Demo销毁时,它才可以释放string。

  令一个类展现类似指针的行为的最好的办法是使用shared_ptr来管理类中的资源。拷贝或赋值一个shared_ptr会拷贝(赋值)shared_ptr所指向的指针。shared_ptr类自己记录有多少用户共享它所指向的对象。当没有用户使用对象时,shared_ptr类负责释放资源。

  但是,有时我们希望直接管理资源。在这种情况下,使用引用计数就很有用了。为了说明引用计数如何工作,我们将重新定义Demo,令其行为像指针一样,但我们不使用shared_ptr,而是设计自己的引用计数。

1)引用计数

   引用计数的工作方式如下:

  • 除了初始化对象外,每个构造函数(拷贝构造函数除外)还要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
  • 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
  • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。

  计数器要保存在动态内存中。当创建一个对象时,我们分配一个新的计数器。当拷贝或赋值对象时,我们拷贝指向计数器的指针。使用这种方法,副本和原对象都会指向相同的计数器。

 1 #include <iostream>
 2 #include <string>
 3 #include <memory>
 4 
 5 class Demo
 6 {
 7 public:
 8     Demo(const std::string &s = std::string()):
 9         ps(new std::string(s)), data(0), use(new std::size_t(1)){}
10     
11     Demo(const Demo &rhs): // 
12         ps(new std::string(*rhs.ps)), data(rhs.data), use(rhs.use){
13         ++*use;
14     }
15 
16     Demo &operator=(const Demo &rhs)
17     {
18         ++*rhs.use; // 先递增右侧运算对象的计数器
19         if (--*use==0) // 递减本对象的计数器
20         {
21             std::cout << "operator= delete " << *ps << std::endl;
22             delete ps;
23             delete use;
24         }
25         ps = rhs.ps;
26         data = rhs.data;
27         use = rhs.use;
28         return *this;
29     }
30 
31     ~Demo()
32     {
33         if (--*use == 0)
34         {
35             std::cout << "~Demo " << *ps << std::endl;
36             delete ps;
37             delete use;
38         }
39     }
40     std::string *ps;
41     int data;
42     std::size_t *use; // 计数器
43 };
44 
45 int main()
46 {
47     Demo a("hi"), b("hello");
48     a = b;
49     std::cout << "-----------" << std::endl;
50     return 0;
51 }
View Code

二、交换操作

  除了定义拷贝控制成员,管理资源的类通常还定义一个名为swap的函数。对于那些与重排元素顺序的算法一起使用的类,定义swap是非常重要的。这类算法在交换两个元素时会调用swap。

  如果一个类定义来了自己的swap,那么算法将使用类自定义版本。否则,算法将使用标准库定义的swap。

1)自定义swap函数

 1 #include <iostream>
 2 #include <string>
 3 #include <memory>
 4 
 5 class Demo
 6 {
 7     friend void swap(Demo &, Demo &);
 8 public:
 9     Demo(const std::string &s = std::string()) :
10         ps(new std::string(s)), data(0){}
11     Demo(const Demo &rhs) : // 每个字符串都有自己的拷贝
12         ps(new std::string(*rhs.ps)), data(rhs.data){}
13     Demo &operator=(Demo rhs)
14     {
15         swap(*this, rhs);
16         return *this;
17     }
18     std::string *ps;
19     int data;
20 };
21 
22 inline void swap(Demo &lhs, Demo &rhs)
23 {
24     std::cout << "swap" << std::endl;
25     using std::swap;
26     swap(lhs.ps, rhs.ps); // 交换指针
27     swap(lhs.data, rhs.data); // 交换int成员
28 }
29 
30 int main()
31 {
32     Demo a, b;
33     *b.ps = "bb";
34     a = b;
35     std::cout << *a.ps << "  " << *b.ps << std::endl;
36     *b.ps = "cc";
37     std::cout << *a.ps << "  " << *b.ps << std::endl;
38     return 0;
39 }
View Code

我们将swap定义为friend,以便能访问Demo的所有成员。由于swap的存在就是为了优化代码,我们将其声明为inline函数。

  注意:与拷贝控制成员不同,swap不是必要的。但是,对于分配了资源的类,定义swap可能是一种很重要的优化手段。

2)swap函数应该调用swap,而不是std::swap

  在本例中,数据成员是内置类型的,而内置类型是没有特定版本的swap的,所以在本例中,对swap调用会调用标准库std::swap。但是,如果一个类的成员有自己类型特定的swap函数,调用std::swap就是错误的了(对于某些类,调用std::swap会进行不必要的拷贝)。

  每个swap调用都应该是未加限定的。即,每个调用都应该是swap,而不是std::swap。如果存在类型特定的swap版本,其匹配程度会优于std中定义的版本。

3)在赋值运算符中使用swap

  定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换。

  这个技术自动处理了自赋值情况并且天然就是异常安全的。它通过在改变左侧运算对象之前拷贝右侧运算对象保证了自赋值的正确,这与我们在原来的赋值运算符中使用的方法是一致的。代码中唯一可能抛出异常的是拷贝构造函数中的new表达式。如果真发生了异常,它会在我们改变左侧运算对象之前发生。

三、对象移动

  新标准的一个最主要的特性是可以移动而非拷贝对象的能力。很多情况下都会发生对象拷贝,在其中某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。

  在旧C++标准中,没有直接的方法移动对象。因此,在旧的标准库中,容器中所保存的类必须是可拷贝的。但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。

1、右值引用

  为了支持移动操作,新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。右值引用有一个重要的性质——只能绑定到一个将要销毁的对象

  一般而言,一个左值表达式表示的是一个对象的身份(在内存中的位置),一个右值表达式表示的是对象的值(内容)。

  一个右值引用是某个对象的另一个名字。对于常规引用(为了与右值引用区分开,我们称之为左值引用),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

 1 #include <iostream>
 2 #include <string>
 3 #include <memory>
 4 
 5 int main()
 6 {
 7     int i = 42;
 8     int &r = i; // r引用i
 9     //int &&r = i; // 错误:不能将一个右值引用绑定到一个左值上
10     //int &r2 = i * 42; // 错误:i*42是一个右值
11     const int &r3 = i * 42; // 可以将一个const引用绑定到一个右值上
12     int &&rr2 = i * 42; // 将rr2绑定到右值上
13     return 0;
14 }
View Code

  返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上

  返回非左值引用类型的函数,连同算术、关系、位及后置递增/递减运算符,都生成右值。我么不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用绑定或者一个右值引用绑定到这类表达式上

1)左值持久;右值短暂

  左值有持久的状态,而右值要么是字面值常量,要么是在表达式求值过程中创建的临时对象。

  由于右值引用只能绑定到临时对象,我们得知:

  • 所引用的对象将要被销毁。
  • 该对象没有其他用户。

  这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

2)变量是左值

  变量可以看作是只有一个运算对象而没有运算符的表达式,变量表达式都是左值。因此,我们不能将一个右值引用绑定到一个右值引用类型的变量上。

 1 #include <iostream>
 2 #include <string>
 3 #include <memory>
 4 
 5 int main()
 6 {
 7     int &&r1 = 42; // 字面值常量是右值
 8     //int &&r2 = r1; // 错误:表达式r1是左值
 9     return 0;
10 }
View Code

3)标准库move函数

  虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数位于头文件utility中。

 1 #include <iostream>
 2 #include <string>
 3 #include <utility>
 4 
 5 int main()
 6 {
 7     int &&r1 = 42; // 字面值常量是右值
 8     int &&r2 = std::move(r1);
 9     return 0;
10 }
View Code

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。调用move就意味着承诺:除了对移后源对象(在这里指r1)赋值或销毁它外,我们将不再使用它。在调用move后,我们不能对移后源对象的值做任何假设

  注意:使用move的代码应该使用std::move而不是move。这样做可以避免潜在的名字冲突。

2、移动构造函数和移动赋值运算符

  类似string类(及其他标准库类),如果我们自己的类也同时支持移动和拷贝,那么也能从中受益。为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,但它们从对象“窃取”资源而不是拷贝资源。

  类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须由默认实参。

  除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象

   作为一个例子,我们为StrVec类定义移动构造函数:

1 StrVec::StrVec(StrVec &&s)noexcept // 移动操作不应抛出任何异常
2 // 成员初始化器接管s中的资源
3     : elements(s.elements), first_free(s.first_free), cap(s.cap)
4 {
5     // 令s进入这样的状态——对其运行析构函数是安全的
6     s.elements = s.first_free = s.cap = nullptr;
7 }
View Code

  与拷贝构造函数不同,移动构造函数不分配任何内存;它接管给定的StrVec中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行析构函数。

1)移动操作、标准库容器和异常

  由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。当编写一个不抛出异常的移动操作时,我们应该将此事通知标准库。我们将看到,除非标准库知道我们的移动构造函数不会抛出异常,否则它会认为移动我们的类对象时可能会抛出异常,并且为了处理这种可能性而做一些额外的操作。

  一种通知标准库的方法是在我们的构造函数中指明noexcept。noexcept是新标准引入的,是承诺一个函数不抛出异常的方法。我们在一个函数的参数列表后指定noexcept。在一个构造函数中,noexcept出现在参数列表和初始化列表开始的冒号之间。

  必须在类头文件的声明中和定义中(如果在定义在类外的话)都指定noexcept

  不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。

2)移动赋值运算符

  移动赋值运算符执行与析构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值。我们不能在使用右侧运算对象的资源之前就释放左侧运算对象的资源(可能是相同的资源)。

 1 StrVec &StrVec::operator=(StrVec &&rhs)noexcept
 2 {
 3     if (this != &rhs)
 4     {
 5         free();
 6         elements = rhs.elements;
 7         first_free = rhs.first_free;
 8         cap = rhs.cap;
 9         rhs.elements = rhs.first_free = rhs.cap = nullptr;
10     }
11     return *this;
12 }
View Code

3)移后源对象必须可析构

  从一个对象移动数据并不会销毁此对象,但有时在移动操作完成后,源对象会被销毁。因此,当我们编写一个移动操作时,必须确保移后源对象进入一个可析构的状态。

  除了将移后源对象置为析构安全的状态之外,移动操作还必须确保对象仍然是有效的。一般来说,对象有效就是指可以安全地为其赋予新值或者可以安全地使用而不依赖其当前值。另一方面,移动操作对移后源对象中留下的值没有任何要求。因此,我们的程序不应该依赖于移后源对象中的数据

4)合成的移动操作

  只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员

 1 #include <iostream>
 2 #include <string>
 3 
 4 // 编译器会为X和HasX合成移动操作
 5 class X {
 6 public:
 7     int i; // 内置类型可以移动
 8     std::string s; // string定义了自己的移动操作
 9 };
10 class HasX {
11 public:
12     X mem;// X有合成的移动操作
13 };
14 int main()
15 {
16     X x, x2 = std::move(x); // 使用合成的移动构造函数
17     HasX hx, hx2 = std::move(hx); // 使用合成的移动构造函数
18     return 0;
19 }
View Code

  与拷贝操作不同,移动操作永远也不会隐式地定义为删除的函数。但是,如果我们显式地要求编译器生成=default的移动操作,且编译器不能移动所有成员,则编译器会将移动操作定义为删除的函数。移动操作定义为删除的函数的原则:

  • 与拷贝构造函数不同,移动构造函数被定义为删除的函数的条件是:有类成员定义了自己的拷贝构造函数且未定义移动构造函数,或者是有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数。移动赋值运算符的情况类似。
  • 如果有类成员的移动构造函数或移动赋值运算符被定义为删除的或是不可访问的,则类的移动构造函数或移动赋值运算符被定义为删除的。
  • 类似拷贝构造函数,如果类的析构函数被定义为删除的或不可访问的,则类的移动构造函数被定义为删除的。
  • 类似拷贝赋值运算符,如果有类成员是const的或引用,则类的移动赋值运算符被定义为删除的。

  定义了一个移动构造函数或移动赋值运算符的类必须定义自己的拷贝操作。否则,这些成员默认地被定义为删除的

5)移动右值,拷贝左值

  如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值的情况类似。

6)如果没有移动构造函数,右值也被拷贝

  如果一个类有一个拷贝构造函数但未定义移动构造函数,会发生什么呢?在此情况下,编译器不会合成移动构造函数,这意味着此类将有拷贝构造函数但不会有移动构造函数。如果一个类没有移动构造函数,函数匹配规则保证该类型的对象会被拷贝,即使我们试图通过调用move来移动它们时也是如此,其对象是通过拷贝构造函数来“移动”的。拷贝赋值运算符和移动移动赋值运算符的情况类似。

7)拷贝并交换赋值运算符和移动操作

  我们的Demo版本定义了一个拷贝并交换赋值运算符,它是函数匹配和移动操作间相互关系的一个很好的示例。

 1 #include <iostream>
 2 #include <string>
 3 #include <memory>
 4 
 5 class Demo
 6 {
 7     friend void swap(Demo &, Demo &);
 8 public:
 9     Demo(const std::string &s = std::string()) :
10         ps(new std::string(s)){}
11     Demo(const Demo &rhs) :
12         ps(new std::string(*rhs.ps)) {
13         std::cout << "拷贝构造函数" << std::endl;
14     }
15     Demo(Demo &&rhs)noexcept :ps(rhs.ps) {
16         rhs.ps = nullptr;
17         std::cout << "移动构造函数" << std::endl;
18     }
19     Demo &operator=(Demo rhs)
20     {
21         std::cout << "=" << std::endl;
22         swap(*this, rhs);
23         return *this;
24     }
25     ~Demo() {
26         delete ps;
27     }
28     std::string *ps;
29 };
30 
31 inline void swap(Demo &lhs, Demo &rhs)
32 {
33     std::cout << "swap" << std::endl;
34     using std::swap;
35     swap(lhs.ps, rhs.ps); // 交换指针
36 }
37 
38 int main()
39 {
40     Demo hp, hp2;
41     hp = hp2; // hp2是一个左值;hp2通过拷贝构造函数调用
42     std::cout << "--------------------" << std::endl;
43     hp = std::move(hp2); // 移动构造函数移动hp2
44     return 0;
45 }
View Code

  观察赋值运算符。此运算符有一个非引用参数,这意味着此参数要进行拷贝初始化。依赖于实参的类型,拷贝初始化要么使用拷贝构造函数,要么使用移动构造函数——左值被拷贝,右值被移动。因此,单一的赋值运算符就实现了拷贝赋值运算符和移动赋值运算符两种功能。

8)移动迭代器

  先标准库中定义了一种移动迭代器适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配此迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用

  我们通过调用标准库的make_move_iterator函数将一个普通迭代器转换为一个移动迭代器。此函数接受一个迭代器参数,返回一个移动迭代器。原迭代器的所有其他操作在移动迭代器中都照常工作。

 1 void StrVec::reallocate()
 2 {
 3     /*
 4     // 不使用移动迭代器的版本
 5     auto newcapacity = size() ? 2 * size() : 1;
 6     auto newdata = alloc.allocate(newcapacity);
 7     auto dest = newdata;
 8     auto elem = elements;
 9     for (std::size_t i = 0; i != size(); ++i)
10         alloc.construct(dest++, std::move(*elem++));
11     free();
12     elements = newdata;
13     first_free = dest;
14     cap = elements + newcapacity;*/
15     // 使用移动迭代器的版本
16     auto newcapacity = size() ? 2 * size() : 1;
17     auto first = alloc.allocate(newcapacity);
18     auto last = std::uninitialized_copy(std::make_move_iterator(begin()), std::make_move_iterator(end()), first);
19     free(); // 释放旧空间
20     elements = first;
21     first_free = last;
22     cap = elements + newcapacity;
23 }
View Code

  值得注意的是,标准库不保证哪些算法适用于移动迭代器,哪些不适用。由于移动一个对象可能销毁掉原对象,因此你只有在确信算法在为一个元素赋值或将其传递给一个用户定义的函数后不再访问它时,才能将移动迭代器传递给算法。

3、右值引用和成员函数

  除了构造函数之外,如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。这种允许移动的成员函数通常使用与拷贝/移动构造函数和赋值运算符相同的参数模式——一个版本接受一个指向const的左值引用,第二个版本接受一个指向非const的右值引用。

  一般来说,我们不需要为函数操作定义接受一个const X&&或是一个普通的X&参数的版本。当我们希望实参“窃取”数据时,通常传递一个右值引用。为了达到这一目的,实参不能是const的。类似的,从一个对象进行拷贝的操作不应该改变该对象。因此,通常不需要定义一个接受普通的X&参数版本。

  我们为StrVec类定义另一个版本的push_back:

 1 void StrVec::push_back(const std::string &s)
 2 {
 3     std::cout << "push_back--copy" << std::endl;
 4     check_n_alloc();
 5     alloc.construct(first_free++, s); // 在first_free指向的元素中构造s的一个副本
 6 }
 7 
 8 void StrVec::push_back(std::string &&s)
 9 {
10     std::cout << "push_back--move" << std::endl;
11     check_n_alloc();
12     alloc.construct(first_free++, std::move(s));
13 }
View Code

  当我们调用push_back时,实参类型决定了新元素是拷贝还是移动到容器中:

 1 #include <iostream>
 2 #include <string>
 3 #include "header/StrVec.h"
 4 
 5 // 静态变量在类外一定要定义
 6 std::allocator<std::string> StrVec::alloc;
 7 int main()
 8 {
 9     StrVec vec;
10     std::string s = "hello world";
11     vec.push_back(s); // 调用push_back(const std::string &)
12     vec.push_back("QAQ"); // 调用push_back(std::string &&)
13     return 0;
14 }
View Code

1)右值和左值引用成员函数

  通常我们在一个对象上调用成员函数,而不管该对象是一个左值还是一个右值。例如:

 1 #include <iostream>
 2 #include <string>
 3 
 4 int main()
 5 {
 6     std::string s1 = "a value", s2 = "annother";
 7     auto n = (s1 + s2).find('a'); // 在一个string右值上调用find成员
 8     std::cout << n << std::endl;
 9     s1 + s2 = "wow"; // 对一个右值进行了赋值
10     return 0;
11 }
View Code

  在旧标准中,我们没办法阻止这种使用方式。为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是,我们可能希望自己的类中阻止这种用法。在此情况下,我们希望强制左侧运算对象(即this执行的对象)是一个左值。

  我们希望指出this的左值/右值属性的方式与定义const成员函数相同,即,在参数列表后放置一个引用限定符。引用限定符可以是&或&&,分别指出this可以指向一个左值或右值。类似const限定符,引用限定符只能用于非static成员函数,且必须同时出现在函数的声明和定义中。

1 class Foo {
2 public:
3     Foo &operator=(const Foo &)&; // 只能向可修改的左值赋值
4 };
5 Foo &Foo::operator=(const Foo &rhs)&
6 {
7     //指向将rhs赋予本对象的操作
8     return *this;
9 }
View Code

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

1 class Foo {
2 public:
3     Foo some_mem()const &;
4 };
View Code

2)重载和引用函数

   就像一个成员函数可以根据是否有const来区分其重载版本一样,引用限定符也可以区分重载版本。而且,我们可以综合利用const来区分一个成员函数的重载版本。

 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 #include <algorithm>
 5 
 6 class Foo {
 7 public:
 8     Foo sorted() && ; // 可用于改变的右值
 9     Foo sorted() const &; // 可用于任何类型的Foo
10 private:
11     std::vector<int> data;
12 };
13 // 本对象为右值,因此可以原址改变
14 Foo Foo::sorted() &&
15 {
16     sort(data.begin(), data.end());
17     return *this;
18 }
19 // 本对象是const或是一个左值,哪种情况我们都不能对其进行原址排序
20 Foo Foo::sorted()const&
21 {
22     Foo ret(*this);
23     sort(ret.data.begin(), ret.data.end()); // 排序副本
24     return ret; // 返回副本
25 }
26 
27 int main()
28 {
29     
30     return 0;
31 }
View Code

  当我们定义const成员函数时,可以定义两个版本,唯一的差别是一个版本有const限定而另一个没有。引用限定的函数则不一样,如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。

原文地址:https://www.cnblogs.com/ACGame/p/10293428.html