1. 模板安全(续)
在异常安全的第二部分,我讲了在构造函数和析构函数中导致资源泄漏的问题。这次将探索另外两个问题。并且以推荐读物列表结束。
1.1 Problem #2:get
上次,我定义X::get()为:
T get()
{
return *value_;
}
这个定义有点小小的不足。既然get()不改变wrapper对象,我应该将它申明为const成员的:
T get() const
{
return *value_;
}
get()返回了一个T的临时对象。这个临时对象通过T的拷贝构造函数根据*value_隐式生成的,而这个构造函数可能抛异常。要避开这点,我们应该将get()修改为不返回任何东西:
void get(T &value) const throw()
{
value = *value_;
}
现在,get()接受一个事先构造好的T对象的引用,并通过引用“返回”结果。因为get()现在不调用T的构造函数了,它是异常安全的了。
真的吗?
很不幸,答案是“no”。我们只是将一个问题换成了另外一个问题而已,因为语句
value = *value_;
实际上是
value.operator=(*value_);
而它可能抛异常。更完备的解决方法是
void get(T &value) const throw()
{
try
{
value = *value_;
}
catch (...)
{
}
}
现在,get()不会将异常漏出去了。
不过,工作还没完成。在operator=给value赋值时抛异常的话,value将处于不确定状态。get()想要有最大程度的健壮接口的话,它必须两者有其一:
l value根据*value_进行了完全设置,或
l value没有被改变。
这两条要将我们弄跳起来了:无论我们用什么方法来解决这个问题,我们都必须调用operator=来设置value,而如果operator=抛了异常,value将只被部分改变。
我们的这个强壮接口看起来美却不实在。我们无法简单地实现它,只能提供一个弱些的承诺了:
l value根据*value_进行了完全设置,或
l value处于一个不确定的(错误)状态。
但还有一个问题没解决:让调用者知道回传的value是否是“好的”。一个可能的解决方法(也很讽刺的)是抛出一个异常。另外一个可能方法,也是我在这儿采用的方法是返回一个错误码。
修改后的get()是:
bool get(T &value) const throw()
{
bool error(false);
try
{
value = *value_;
}
catch (...)
{
error = true;
}
return error;
}
提供了一个较弱的承诺的这个新接口是安全的。它行为安全吗?是的。wrapper所拥有的唯一资源是分配给*value_的内存,而它是受保护的,即使operator=抛了异常。
符合最初的说明,get()有了一个健壮的异常安全承诺,即使T没有这个承诺。最终,我们过于加强了get()的承诺(这取决于value),而应该将它降低到T的承诺层次。我们用一个警告修正get()的承诺,基于我们不能控制或不能预知T的状态。In the end, we over-committed get's guarantee (the determinism of value), and had to bring it down to T's level. We amended get's contract with a caveat, based on conditions in T we couldn't control or predict.
原则:程序的健壮性等于它最弱的承诺。尽可能提供最健壮的承诺,同时在行为和接口上。
推论:如果你自己的接口的承诺比其他人的接口健壮,你通常必须将你的接口减弱到相匹配的程度。
1.2 Problem #3:set
我们现在的X::set()的实现是:
void set(T const &value)
{
*value_ = value;
}
(和get()不同,set()确实修改wrapper对象,所以不能申明为cosnt。)
语句
*value_ = value;
应该看起来很熟悉:她只是前面Problem #2中提到的语句
value = *value_;
的反序。注意到这个变化,Problem #3的解决方案就和Problem #2的一样了:bool set(T const &value) throw()
{
bool error(false);
try
{
*value = value_;
}
catch (...)
{
error = true;
}
return error;
}
和我们在get()中回传value遇到的问题一样:如果operator=抛了异常,我们无法知道*value_的状态。我们对get()的承诺的警告在这儿同样适用。
get()和set()现在有这同样的操作但不同的用途:get()将当前对象的值赋给另外一个对象,而set()将另外一个对象的值赋给当前对象。由于这种对称性,我们可以将共同的代码放入一个assign()函数:
static bool assign(T &to, T const &from) throw()
{
bool error(false);
try
{
to = from;
}
catch (...)
{
error = true;
}
return error;
}
使用了这个辅助函数后,get()和set()缩短为
bool get(T &value) const throw()
{
return assign(value, *value_);
}
bool set(T const &value) throw()
{
return assign(*value_, value);
}
1.3 最终版本
wrapper的最终版本是
template <typename T>
class wrapper
{
public:
wrapper() throw()
: value_(NULL)
{
try
{
value_ = new T;
}
catch (...)
{
}
}
~wrapper() throw()
{
try
{
delete value_;
}
catch (...)
{
operator delete(value_);
}
}
bool get(T &value) const throw()
{
return assign(value, *value_);
}
bool set(T const &value) throw()
{
return assign(*value_, value);
}
private:
bool assign(T &to, T const &from) throw()
{
bool error(false);
try
{
to = from;
}
catch (...)
{
error = true;
}
return error;
}
T *value_;
wrapper(wrapper const &);
wrapper &operator=(wrapper const &);
};
(哇!52行,原来只有20行的!而且这还只是一个简单的例子。)
注意,所有的异常处理函数只是吸收了那些异常而没有做任何处理。虽然这使得wrapper异常安全,却没有纪录下导致这些异常的原因。
我在Part13中讲的在构造函数上的相冲突的原则在这儿同样适用。异常安全是不够的,并且实际上是达不到预期目的的,如果它掩盖了最初的异常状态的话。同时,如果异常对象在被捕获前就弄死了程序的话,大部分的异常恢复方案都将落空。最后,良好的设计必须满足下两个原则:
l 通过异常对象的存在来注视异常状态,并适当地做出反应。
l 确保创造和传播异常对象不会造成更大的破坏。(别让治疗行为比病本身更糟糕。)
1.4 其它说法
在过去3部分中,我剖析了异常安全。我强烈建议你读一下这些文章:
l The first principles of C++ exception safety come from Tom Cargill's "Exception Handling: A False Sense of Security," originally published in the November and December 1994 issues of C++ Report. This article, more than any other, alerted us to the true complexities and subtleties of C++ exception handling.
l C++ Godfather Bjarne Stroustrup is writing an exception-safety Appendix for his book The C++ Programming Language (Third Edition) (). Bjarne's offering a draft version () of that chapter on the Internet.
l I tend to think of exception safety in terms of contracts and guarantees, ideas formalized in Bertrand Meyer's "Design by Contract" () programming philosophy. Bertrand realizes this philosophy in both his seminal tome Object-Oriented Software Construction () and his programming language Eiffel (http://{域名已经过期}/eiffel/page.html).
l Herb Sutter has written the most thorough C++ exception-safety treatise I've seen. He's published it as Items 8-19 of his new book Exceptional C++ (http://{域名已经过期}/asp/bookinfo/bookinfo.asp?theisbn=0201615622). If you've done time on Usenet's comp.lang.c++.moderated newsgroup, you've seen Herb's Guru of the Week postings. Those postings inspired the bulk of his book. Highly recommended.
l Herb's book features a forward written by Scott Meyers. Scott covers exception safety in Items 9-15 of his disturbingly popular collection More Effective C++ (http://{域名已经过期}/asp/bookinfo/bookinfo.asp?theisbn=020163371X). If you don't have this book, you simply must acquire it; otherwise Scott's royalties could dry up, and he'd have to get a real job like mine.
Scott(在他的Item14)认为,不应该将异常规格申明加到模板成员上,和我的正相反。事实是无论用不用异常规格申明,总有一部分程序需要保护所有异常,以免程序自毁。Scott公正地指出不正确的异常规格申明将导致std::unexpected――这正是他建议你避开的东西;但,在本系列的Part11,我指出unexpected比不可控的异常传播要优越。
最后要说的是,这儿不会只有一个唯一正确的答案的。我相信异常规格申明可以导致更可预知和有限度的异常行为,即使是对于模板。我也得坦率地承认,在异常/模板混合体上我也没有足够经验,尤其是对大系统。我估计还很少有人有这种经验,因为(就我所知)还没有哪个编译器支持C++标准在异常和模板上的全部规定。