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) (http://www.research.att.com/~bs/3rd.html). Bjarne's offering a draft version (http://www.research.att.com/~bs/3rd_safe0.html) 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" (http://www.eiffel.com/doc/manuals/technology/contract/page.html) programming philosophy. Bertrand realizes this philosophy in both his seminal tome Object-Oriented Software Construction (http://www.eiffel.com/doc/oosc.html) and his programming language Eiffel (http://www.eiffel.com/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://www1.fatbrain.com/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://www1.fatbrain.com/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++标准在异常和模板上的全部规定。