条款10: 令operator=返回一个reference to *this
Have assignment operators return a 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; } };
这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:
class Widget { public: ... Widget& operator+=(const Widget& rhs) { // 这个协议适用于+=,-=,*=等等 ... return *this; } Widget& operator=(int rhs) { // 此函数也适用,即使参数类型不符规定 ... return *this; } };
这只是个协议,并无强制性.如果不遵循它,代码一样可通过编译.然而这份协议被所有内置类型和标准程序库提供的类型共同遵守,所以最好还是遵守它.
注意:
令赋值(assignment)操作符返回一个reference to *this.
条款11: 在 operator=中处理"自我赋值"
Handle assignment to self in operator=
"自我赋值"发生在对象被赋值给自己时:
class Widget { ... }; Widget w; ... w = w; // 赋值给自己
这看起来有点愚蠢,但它合法,所以不要认定客户绝不会那么做.此外赋值动作并不总是那么可被一眼辨识出来,例如:
a[i] = a[j]; // 潜在的自我赋值
而过i和j具有相同的值,这便是个自我赋值.再看:
*px = *py; // 潜在的自我赋值
如果px和py恰巧指向同一个东西,这也是自我赋值.这些并不明显的自我赋值,是"别名"(aliasing)带来的结果:所谓"别名"就是"有一个以上的方法指向某对象".一般而言如果某段代码操作pointers或references而它们被用来"指向多个相同类型的对象",就需要考虑这些对象是否为同一个.实际上两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成"别名",因为一个base class 的reference或pointer可以指向一个derived
class 对象:
class Base { ... }; class Derived : public Base { ... }; void doSomething(const Base& rb, Derived* pd); // rb和*pd可能是同一对象
如果尝试自行管理资源,可能会掉进"在停止使用资源之前以外释放了它"的陷阱.假设建立一个 clas 来保存一个指针指向一块动态分配的位图:
class Bitmap { ... }; class Widget { ... private: Bitmap* pb; // 指针,指向一个从heap分配而得的对象 };
下面是 operator= 实现代码,表面上看起来合理,但自我赋值出现时就不完全:
Widget &Widget::operator=(const Widget& rhs) { // 一份不安全的operator=实现版本 delete pb; pb = new Bitmap(&rhs.pb); return &this; }
这里的自我赋值问题是,operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象.如果这样的话,delete 就不只是销毁当前对象的bitmap,它也销毁rhs的bitmap.在函数末尾,Widget发现自己持有一个指针指向一个已被删除的对象.
欲阻止这种错误,传统做法是借由 operator=最前面的一个"证同测试"达到"自我赋值"的检验目的:
Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; }
这样做行得通.但这个新版本仍然存在异常方面的麻烦.如果"new Bitmap"导致异常(不论是因为分配时内存不够或因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap.这样的指针有害,无法安全地删除它们,甚至无法安全地读取它们.
令人高兴的是,让 operator=具备"异常安全性"往往自动获得"自我赋值安全"的的回报.因此只需要把焦点放在实现"异常安全性"上.例如以下代码,只需要注意在复制pb所指东西之前别删除pb:
Widget& Widget::operator=(const Widget& rhs) { Bitmap* pOrig = pb; // 记住原先的pb pb = new Bitmap(*rhs.pb); // 令pb指向*pb的一个副本 delete pOrig; // 删除原先的pb return *this; }
现在,如果"new Bitmap"抛出异常,pb就维持原状.即使没有证同测试,这段代码还是能够处理自我赋值,因为对原bitmap做了一份复件,删除原bimap,然后指向新制造的那个复件.它或许不是处理"自我赋值"的最高效办法,但它行得通.
在 operator=函数内手工排列语句(确保代码不但"异常安全"而且"自我赋值安全")的一个替代方案是,使用所谓的copy and swap技术.这个技术和"异常安全性"有密切关系,所以由条款29详细说明.然而由于它是一个常见而够好的 operator=撰写方法,所以值得看看其实现手法像什么样子:
class Widget { ... void swap(Widget& rhs); // 交换*this和rhs的数据,详见条款29 }; Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); // 为rhs数据制作一份副本 swap(temp); // 将*this数据和上述副本的数据交换 return *this; }
这个主题的另一个变奏曲利用了以下事实:(1)某 class 的copy assignment操作符可能被声明为"以by value方式接受实参";(2)以by value方式传递东西会造成一份副本(详见条款20);
Widget& Widget::operator=(Widget rhs) { // rhs是被传对象的一份副本 swap(rhs); // pass by value return *this; }
它为了伶俐巧妙的修补而牺牲了清晰性,然而将"copying动作"从函数本体内移至"函数参数构造阶段"却可令编译器有时生成更高效的代码.
(高效是否类似于返回值优化????)
注意:
确保当对象自我赋值时 operator=有良好行为.其中技术包括比较"来源对象"和"目标对象"的地址,精心周到的语句顺序,以及copy-and-swap.
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确.
条款12: 复制对象时勿忘其每一个成分
Copy all part of an object
设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,那便是带着适切名称的copy构造函数和copy assignment操作符.称它们为copying函数.条款5观察到编译器会在必要时候为class创建copying函数,并说明这些"编译器生成版"的行为:将被拷贝对象的所有成员变量都做一份拷贝.
如果拒绝编译器写出copying函数,则需要在copying函数对所有变量都进行拷贝.为 class 添加一个成员变量,必须同时修改copying函数.
因此在"为derived class撰写copying函数"时一定要非常小心地复制其base class 成分.那些成分往往是 private,所以无法直接访问它们,应该让derived class 的copying函数调用相应的base class 函数:
DerivedCustomer& DerivedCustom::operator=(const DerivedCustom& rhs) { Custom::operator=(rhs); ... return *this; }
本条款所说的"复制每一个成分"现在应该很清楚了.当编写一个copying函数,确保(1)复制所有local成员变量,(2)调用所有base classes内的适当的copying函数.
如果发现copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者使用.这样的函数往往是 private 而且常被命名为init.这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复.
注意:
copying函数应该确保复制"对象内的所有成员变量"以及"所有base class成分".
不要尝试以某个copying函数实现另一个copying函数.应该将共同机能放进第三个函数中,并由两个copying函数共同调用.
版权声明:本文为博主原创文章,未经博主允许不得转载。