第13章 拷贝控制(下)

13.3 交换操作

除了定义拷贝控制成员,管理资源的类通常还定义了一个名为swap的函数。为了交换两个对象我们需要进行一次拷贝和两次赋值

这段代码将原来v1中的string拷贝了两次——第一次是HasPtr的拷贝构造函数将v1拷贝给temp,第二次是赋值运算符将temp赋予v2。将v2赋予v1的语句还拷贝了原来v2中的string。
理论上,这些内存分配都是不必要的。我们更希望swap交换指针,而不是分配string的新副本:

  1. 编写我们自己的swap函数

    我们首先将swap定义为friend以便能访问HasPtr的数据成员。由于swap的存在就是为了优化代码,我们将其声明为inline函数。
  2. swap函数应该调用swap,而不是std::swap
    非常仔细的读者可能会奇怪为什么swap函数中的using声明没有隐藏HasPtr版本swap的声明。我们将在18.2.3节解释为什么这段代码能正常工作。
  3. 在赋值运算符中使用swap
    定义swap的类通常用swap来定义它们的赋值运算符。这些运算符使用了一种名为拷贝并交换(copy and swap)的技术。这种技术将左侧运算对象与右侧运算对象的一个副本进行交换:

    在这个版本的赋值运算符中,参数并不是一个引用,我们将右侧运算对象以传值方式传递给了赋值运算符。因此,rhs是右侧运算对象的一个副本。
13.4 拷贝控制示例

我们将Folder类的设计留作练习。但是,我们假定Folder类包含名为addMsg和remMsg成员,分别在完成给定Folder对象的消息集合中添加和删除Message的工作。

  1. Message类
  2. save和remove成员
    除拷贝控制成员外,Message类只有两个公共成员:save,将本Message存放在给定Folder中;remove,删除本Message。
  3. Message类的拷贝控制成员
    当我们拷贝一个Message时,得到的副本应该与原Message出现在相同的Folder中。因此,我们必须遍历Folder指针的set,对每个指向原Message的Folder添加一个指向新Message的指针。
  4. Message的析构函数
  5. Message的拷贝赋值运算符
  6. Message的swap函数
    我们通过两遍扫描folders中每个成员来正确处理Folder指针。第一遍扫描将Message从它们所在的Folder中删除。接下来我们调用swap来交换数据成员。最后对folders进行第二遍扫描来添加交换过的Message:
13.5 动态内存管理类(P464)
13.6 对象的移动

新标准的一个最主要的特性是可以移动而非拷贝对象的能力。在某些情况下对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
使用移动而不是拷贝的另一个原因源于IO类或unique_ptr这样的类。这些类都包括不能被共享的资源。因此,这些类型的对象不能拷贝但可以移动。
NOTE:标准库容器、string和shared_ptr类既支持移动也支持拷贝。IO类和unique_ptr类可以移动但不能拷贝。

  • 右值引用
    为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。我们通过&&而不是&来获得右值引用。右值引用一个重要性质——只能绑定到一个将要销毁的对象。
  • 一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
  • 对于常规引用(我们可以称之为左值引用),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
  • 左值持久;右值短暂
    由于右值引用只能绑定到临时对象,我们得知:(1)所引用的对象将要被销毁;(2)该对象没有其他用户。这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
    NOTE:右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。
  • 变量是左值


    其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。
    NOTE:变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。
  • 标准库move函数 

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

    move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味承诺:除了对rr1赋值或销毁它外,我们将不再使用它。
    NOTE:我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

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

类似string类(及其他标准库类),如果我们自己的类也同时支持移动和拷贝,那么也能从中受益。这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。

  1. 类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
  2. 除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态——销毁它是无害的。
  3. 作为一个例子,我们为StrVec类定义移动构造函数,实现从一个StrVec到另一个StrVec的元素移动而非拷贝:
  4. 移动操作、标准库容器和异常
    由于移动操作“窃取”资源,它通常不分配任何资源。因此,移动操作通常不会抛出任何异常。一种通知标准库的方法是在我们的构造函数中指明noexcept。noexcept是新标准引入的。
    NOTE:不抛出异常的移动构造函数和移动赋值运算符必须标记为noexcept。
  5. 移动赋值运算符
    移动赋值运算符执行与析构函数和移动构造函数相同的工作。
  6. 移后源对象必须可析构
    WARNING:在移动操作之后,移后源对象必须保持有效的、可析构的状态,但是用户不能对其值进行任何假设。
  7. 合成的移动操作
    只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。
    NOTE:只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符
  8. 移动右值,拷贝左值……
    如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。
  9. ……但如果没有移动构造函数,右值也被拷贝

发表评论

电子邮件地址不会被公开。 必填项已用*标注