返回一个 reference to *this。

关于赋值,有趣的是你可以把它们写成连锁形式:

int x, y, z;
x = y - z = 15;	//赋值连锁形式

同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:

x = (y = (z = 15));

这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给X。

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议:

class Widget {
public:
Widget & operator=(const Widget& rhs)	{
	...
	return *this;
}
};

这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如"+=,-=,*=,等等。

使operator=具有异常安全性

假设你建立一个class用来保存一个指针指向一块动态分配的位图(bitmap):

class Bitmap { ... };
class Widget {
private:
Bitmap* pb;	//指针,指向一个从heap分配而得的对象
};

下面是operator实现代码,表面上看起来合理,但自我赋值出现时并不安全,也不具备异常安全性。

这里的自我赋值问题是,operator函数内的*this (赋值的目的端)和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap。

欲阻止这种错误,传统做法是藉由operator=最前面的一个“证同测试(identitytest) ”达到“自我赋值”的检验.

这个新版本仍然存在异常方面的麻烦。更明确地说,如果”new Bitmap"导致异常(不论是因为分配时内存不足或因为Bitnap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap。这样的指针有害。你无法安全地删除它们,甚至无法安全地读取它们。

令人高兴的是,让operator具备“异常安全性”往往自动获得“自我赋值安全”的回报。因此愈来愈多人对“自我赋值”的处理态度是倾向不去管它,把焦点放在实现“异常安全性”(exception safety)上。

使用所谓的copy and swap技术.

提供一个特化的swap函数

所谓swap (置换)两对象值,意思是将两对象的值彼此赋予对方。缺省情况下动作可由标准程序库提供的swap算法完成。其典型实现完全如你所预期:

namespace std {
	template<typename T>	//std:: swap 的典型实现;
	void swap( T& a, T& b)	//置换 a 和 b 的值.
	{
		T temp(a);
		a = b;
		b = temp;
	}
}

这缺省的swap实现版本十分平淡,无法刺激你的肾上腺。它涉及三个对象的复制:a复制到temp, b复制到a,以及temp复制到b。但是对某些类型而言,这些复制动作无一必要;对它们而言swap缺省行为等于是把高速铁路铺设在慢速小巷弄内。

其中最主要的就是“以指针指向一个对象,内含真正数据”那种类型。这种设计的常见表现形式是所谓“pimpl手法”(pimpl是"pointer to implementation"的缩写。如果以这种手法设计Widget class,看起来会像这样:

以操作符复合形式(op=)取代其独身形式(op)

确保 operator 的赋值形式(assignment version)(例如 operator+=)与一个operator 的单独形式(stand-alone)(例如 operator+ )之间存在正常的关系,一种好方法是后者(指 operator+ 译者注)根据前者(指operator+= 译者注)来实现

这很容易:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Rational {
public:
...
Rational& operator+=(const Rational& rhs); 
Rational& operator-=(const Rational& rhs);
};

// operator+ 根据 operator+=实现;

operator+(const Rational& lhs,
          const Rational& rhs)
{
  return Rational(lhs) += rhs;
}

// operator- 根据 operator -= 来实现

const Rational operator-(const Rational& lhs,
                         const Rational& rhs)
{
  return Rational(lhs) -= rhs;
}

在这个例子里,从零开始实现 operator+=和-=,而 operator+ 和 operator- 则是通过调用前述的函数来提供自己的功能。使用这种设计方法,只用维护 operator 的赋值形式就行了。而且如果假设 operator 赋值形式在类的 public 接口里,这就不用让 operator 的单独形式成为类的友元.

如果你不介意把所有的 operator 的单独形式放在全局域里,那就可以使用模板来替代 单独形式的函数的编写:

template<class T>
const T operator+(const T& lhs, const T& rhs)
{
return T(lhs) += rhs; // 参见下面的讨论
}
template<class T>
const T operator-(const T& lhs, const T& rhs) {
return T(lhs) -= rhs; // 参见下面的讨论 }
...

使用这些模板,只要为 operator 赋值形式定义某种类型,一旦需要,其对应的operator独身形式就会被自动生成。

在这里值得指出的是三个效率方面的问题。

  1. 总的来说 operator 的复合形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator 的赋值形式把 结果写到左边的参数里,因此不需要生成临时对象来容纳 operator 的返回值。

  2. 提供 operator 的复合形式的同时也要提供其标准形式,允许类的客户端在便利 与效率上做出折衷选择。也就是说,客户端可以决定是这样编写:

     Rational a, b, c, d, result;
     ...
     result = a + b + c + d;
    

    还是这样编写:

     result = a;
     result += b;
     result += c;
     result += d;
    

    前者比较容易编写、debug 和维护,并且在 80%的时间里它的性能是可以被接受的。后者具有更高的效率,估计这对于汇编语言程序员来说会更直观一些。通过 提供两种方案,你可以让客户端开发人员用更容易阅读的单独形式的 operator 来开发和 debug 代码,同时保留用效率更高的 operator 赋值形式替代单独形式的权力。而且根据 operator 的赋值形式实现其单独形式,这样你能确保当客户端从一种形式切换到另一种形 式时,操作的语义可以保持不变。

  3. 涉及到 operator 单独形式的实现。再看看 operator+ 的实现:

     template<class T>
     const T operator+(const T& lhs, const T& rhs)
     { return T(lhs) += rhs; }
    

    表达式 T(lhs)调用了 T 的拷贝构造函数。它建立一个临时对象,其值与 lhs 一样。这 个临时对象用来与 rhs 一起调用 operator+= ,操作的结果被从 operator+返回。这个代码 好像不用写得这么隐密。这样写不是更好么?

     template<class T>
     const T operator+(const T& lhs, const T& rhs) {
     T result(lhs); // 拷贝 lhs 到 result 中
     return result += rhs; // rhs 与它相加并返回结果 }
    

    这个模板几乎与前面的程序相同,但是它们之间还是存在重要的差别。第二个模板包含 一个命名对象,result。这个命名对象意味着不能在 operator+ 里使用返回值优化。第一种实现方法总可以使用返回值优化,所以编译器为其生成优化代码的可能 就会更大。

现在,必须指出

return T(lhs) += rhs; 

比大多数编译器希望进行的返回值优化更复杂。上面第一个函数实现也有这样的临时对象开销,就象你为使用命名对象 result 而耗费的开销一样。然而匿名对象在比命名对象更容易清除,因此当我们面对在命名对象和临时对象间进行选择时,用临时对象更好一些。它使你耗费的开销不会比命名的对象还多,特别是使用老编译器时,它的耗费会更少。

这里谈论的命名对象、未命名对象和编译优化是很有趣的,但是主要的一点是 operator 的复合形式(operator+=)比独身形式(operator+)效率更高。做为一个库程序设计者,应该两者都提供,作为一个应用程序的开发者,在优先考虑性能时你应该考虑考虑用operator 复合形式代替独身形式。

复制对象时勿忘其每一个成分

当你编写一个函数,请确保(1)复制所有local成员变量,(2)调用所有base classes内的适当的copying 函数。