operator=的正确写法
文章目录
返回一个 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+= 译者注)来实现
这很容易:
|
|
在这个例子里,从零开始实现 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独身形式就会被自动生成。
在这里值得指出的是三个效率方面的问题。
-
总的来说 operator 的复合形式比其单独形式效率更高,因为单独形式要返回一个新对象,从而在临时对象的构造和释放上有一些开销。operator 的赋值形式把 结果写到左边的参数里,因此不需要生成临时对象来容纳 operator 的返回值。
-
提供 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 的赋值形式实现其单独形式,这样你能确保当客户端从一种形式切换到另一种形 式时,操作的语义可以保持不变。
-
涉及到 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 函数。
文章作者 Forz
上次更新 2017-08-31