Effective C++读书笔记
--By Nathan.Yu 2007-11-24--
2 构造/析构/赋值运算(之四)
条款08:别让异常逃离析构函数
C++并不禁止析构函数吐出异常,但它不鼓励你这样做!
理由:
设:std::vector<Widget> v;
假设v中有多个Widget,在销毁v的过程中,当有2个以上的Widget在销毁的时候抛出异常,对C++而言就太多了。在两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为。
只要是在析构函数吐出异常,即使并非使用容器,程序也可能过早结束或出现不明确行为。
如果程序在析构函数中捕获异常,“强迫结束程序,即调用abort”是个合理的选项。可以抢先制“不明确行为”于死地。
另,将异常吞没是个坏主意。因为它压制了“某些动作失败”的重要信息。为了让这成为一个可行的方案,程序必须确保能够继续可靠地执行,即使在遭遇并忽略一个错误之后。
更好地做法是,赋予客户一个处理异常的机会。如果某个操作必须在析构函数中使用,它又可能抛出异常,则将这个操作作为一个公共的接口,允许客户手动调用,让客户自处理异常。在析构函数中,判断客户是否调用过该函数,如若没有,则再次调用,并对可能抛出的异常进行处理。
总之,如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。
请记住:
1、 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它或结束程序。
2、 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
【备注】参见More Effective C++: 条款十一:禁止异常信息(exceptions)传递到析构函数外
条款09:绝不在构造和析构过程中调用virtual函数
<注意>这是C++与Java或C#不同的一个地方
1、Base class构造期间virtual函数绝不会下降到derived classes阶层。
2、在base class构造期间,virtual函数不是virtual函数。
3、在derived class对象的base class构造期间,对象的类型是base class而不是derived class。因此,不止vritual函数会被编译器解析为base class,若使用运行期类型信息(如dynamic_cast或typeid),也会把对象视为base class类型。
4、对象在derived class构造函数开始执行前不会成为一个derived class 对象。
相同道理也适用于析构函数。
解决方案:改用non-virtual函数,并要求derived class构造函数传递必要信息给base class构造函数,base class构造函数中调用non-virtual,该函数根据不同的信息处理不同的derived class。
请记住:
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。
条款10:令operator=返回一个reference to *this
协议:为了实现x=y=z=15之类的“连锁赋值”,赋值操作符必须返回一个引用指向操作符的左侧实参。
这个协议适合于所有赋值相关运算(operator op=)。
请记住:
令赋值(assignment)操作符返回一个reference to *this。
条款11:在operator=中处理“自我赋值”
什么情况发生“自我赋值”?
情况1:Widget w; w = w;
情况2:a[i] = a[j]; //潜在的自我赋值,当i == j时
情况3:*px = *py; //px、py恰巧指向同一个对象。
详细分析operator=时可能出现的情况:
假设类:
class Bitmap{……};
class Widget{
……
private:
Bitmap* pb;
};
operator=的不同实现版本:
版本1:一份不安全的实现版本,不具“自我赋值安全性”,不具“异常安全性”
Widget&
Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这里的自我赋值是,*this和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap了,也销毁rhs的pb。那么*this将持有一个已被删除的对象。
版本2:使用“证同测试(Identity test)”阻止版本1中的错误,达到“自我赋值”的检测目的,具“自我赋值安全性”,不具“异常安全性”
Widget&
Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这里,new Bitmap可能导致异常(不论是因为分配内存不足或因为Bitmap的拷贝构造抛出异常),Widget会持有一个指针指向一块被删除的Bitmap。
版本3:忽视“自我赋值”,把焦点放在实现“异常安全性上”,因为让operator=具备“异常安全”往往自动获得“自我赋值安全”的回报。
<注意>“许多时候一群精心安排的语句就可以导出异常安全(以及自我赋值安全)的代码”,例如:
Widget&
Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb; //记住原先的pb
pb = new Bitmap(*rhs.pb); // 令pb指向*pb的一个副本
delete pOrig; //删除原先的pb
return *this;
}
它或许不是处理“自我赋值”的最高效办法,但它行得通。
版本4:copy and swap技术,处理“自我赋值”的一个简单而通用的技术,并且是“异常安全的”(该版本的详细讨论可参考Exceptional C++)
Widget&
Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
请记住:
1、 确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2、 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正常。
条款12:复制对象时勿忘其每一个成分
如果你为class添加一个成员变量,你必须同时修改copying函数(copy构造,copy赋值),也需要修改所有其他构造函数以及任何非标准形式的operator=。
任何时候只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心的复制其base class成分。那些成分往往是private的,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。
能否令某个copying函数调用另一个copying函数?不能!
令copy assignment操作符调用copy构造函数是不合理的,因为这就像试图构造一个已经存在的对象。
令copy构造函数调用copy赋值操作符同样无意义。构造函数用来初始化新对象,而赋值操作符只施加于已初始化对象身上。
使用private的函数init()来消除重复代码。
请记住:
1、 copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
2、 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。