GotW#82 异常安全(exception safety)和异常规范(exception specifications):值得吗?

类别:编程语言 点击:0 评论:0 推荐:

GotW#82 异常安全(exception safety)和异常规范(exception specifications):值得吗?

原文参见:www.gotw.ca/gotw/082.htm

难度:8/10

花大力气编写对异常安全的代码值得吗?编写异常规范值得吗?你可能会感到惊讶,这些问题仍然备受争议和争论,甚至即使是专家们,意见也不尽一致。

 

问题

JG问题:

1.旧话重提:简短叙述Abrahams异常安全保证(the Abrahams exception safety guarantees)的定义(基本保证,强保证,无抛出保证)。

2.如果违反异常规范会发生什么情况?为什么?论述C++该特性的基本原理。

Guru问题:

3.何时才值得编写符合以下要求的代码:

a)基本保证?b)强保证?c)无抛出保证?

4.何时值得为函数编写异常规范?你会去编写吗?给出你的理由。

 

解答

1.基本保证是指,允许失败操作改变程序状态,但不能有泄漏并且失败操作所影响的对象/模块必须仍然可析构并可用,状态必须是可靠的(consistent)(但不是完全可预测的)。强保证则包括事务式的提交/撤销语义:失败操作必须保证关于该操作的对象的程序状态不被改变。这就意味着对象完全没有被影响,这也包括那些相关外覆(helper)对象的有效性或作用(contents)没有被影响(manipulated),例如指向container的iterator对象。无抛出保证则意味着根本不会发生失败操作。该操作不会抛出异常。

 

2.异常规范的思路是做运行期检查以保证只有特定类型的异常在该函数中抛出(或者根本没有抛出任何异常)。举个例子,下面函数的异常规范保证了f()只抛出A或B类型的异常:

int f() throw( A, B );

如果一个不在“访客”列表("invited-guests" list)中的异常被抛出,函数unexpected()就将被调用。例如:

int f() throw( A, B )

{

        throw C(); // 将调用unexpected()

}

你可以使用标准的set_unexpected()函数来注册自己的处理函数,从而可以应付“非预期异常”(unexpected-exception)的情况。你的该处理函数必须不接受参数并且无返回值。例如:

void MyUnexpectedHandler(){ /*...*/ }

std::set_unexpected( &MyUnexpectedHandler );

接下来的问题就是,你的unexpected处理函数能做些什么呢?一件它不能做的事是,通过普通的函数返回来返回(return via a usual function return)。而它可以做以下两件事:

1)它可以将该异常转换为异常规范所允许的形式。它可以抛出一个满足“异常规范列表”的异常,正是这个“异常规范列表”导致该处理函数被调用的。接下来“栈展开”过程将从函数停止的地方继续下去。

 

2)它可以调用terminate()。(termninate()函数也可以替换掉,但它必须终止程序。)

 

3.编写至少符合一种保证的代码是绝对值得的。有这样几个不错的理由:

1)异常的发生。(来解释流行的说法。)

它们肯定会发生。标准库会抛出异常。语言本身也会抛出异常。我们必须为异常写一些代码。幸运的是,工作量并不大,因为我们现在知道该怎么做了。为此必须养成一些习惯,甚至要不懈地遵循这些习惯——但同时也要注意从错误的代码中学习编程。

一个历来就很棘手的大问题是错误处理。如何报告错误?返回错误代码或抛出异常都可以报告错误。这个问题的细节几乎完全是一种依赖语法的细节,而它们的主要差异正是在于如何报告错误这一语义上,所以每种方法都有各自不同的风格。

 

2)编写对异常安全的代码对你有好处。

对异常安全的代码和良好的代码总是一并出现的。那些帮助我们编写异常安全代码的很普及的技术,正是我们无论如何要做的东西,甚至更多的是和异常无关的。换句话说,异常安全技术对你的代码本身就是有益的,即使根本没有考虑到对异常安全。

看下面的内容,这是我和其它一些人为了使编写异常安全代码更容易而写的,想想这其中的主要技术:

 

l          使用“资源获取初始化”("resource acquisition is initialization")(RAII)来管理资源的分配。使用类似Lock类的资源分配对象以及auto_ptrs通常是个不错的主意。这样做的众多好处中,我们也应该能发现“异常安全”,这并不奇怪。你应该经常可以发现函数中(当然,这里讨论的不是你写的东西,而是其它什么人写的函数)某些程序分支因实施清理动作失败而过早返回,原因就在于清理动作不是使用RAII而自动执行的。

 

l          “从小处做好所有工作,然后保证只使用无异常抛出的操作”从而避免改变程序内部状态,直到你能保证整个的操作都会成功。这样的事务性程序编制方法能编出更明晰,更干净,更安全的代码,即使其中有些错误。你也应该经常可以看到函数中(这里讨论的仍然是其它人的作品,而不是你写的东西)某些程序分支因保存对象状态的操作失败而过早返回,因为早在操作失败之前,程序内部状态已经有些小问题了。

 

l          坚持“一个类(或函数),只做一个任务”。实现多个功能的函数,是很难做到强异常安全(strongly exception-safety)的,例如在《Exceptional C++》中,条款10到18所表述的Stack::Pop()和EvaluateSalaryAndReturnName()函数。很多异常安全的问题可以更简单地解决,或者无意识地消除,只要简单地贯彻“一个函数,只做一个任务”的策略。这一策略正可以应用于异常安全这一方面;这绝对是个好主意,而且是很自然而然的办法。

 

照这么做毫无疑问会使你受益匪浅。

那么我们应当在何时实现何种程度的保证呢?简而言之,这里有一个C++标准库所遵循的策略,你可以颇为有益地将它应用于你自己的代码:

策略

    一个函数应当总是支持最严格的保证,同时不会对不需要这种保证的用户造成伤害。

所以,如果你的函数可以支持无抛出保证而并不影响某些使用者,那么它就应该做到无抛出保证。值得注意的是,一些关键函数,如析构函数和回收函数,绝对必须是无抛出的操作,否则的话,就不可能安全、可靠地执行清理措施。

不然的话,如果你的函数可以支持强保证而并不惩罚某些使用者,那么它就应该做到强保证。注意到,一些函数如vector::insert()一般是做不到强保证的,因为为了实现强保证,我们就必须在每次插入元素时都必须完整复制整个vector的内容。而且不是所有的程序都需要那么注重强保证,这会导致过高的成本。(那些需要强保证的程序可以以强保证的方式“包装”vector::insert(),一般的做法:复制一份vector,在这份拷贝上做插入,一旦成功就和原vector用swap()交换)。

再不然的话,你的函数应该支持基本保证。

关于上面内容的更多信息,诸如:无抛出的swap()是什么样的或者为什么析构函数应该做到无抛出,请参考《Exceptional C++》[1]和《More Exceptional C++》[2]。

 

4.简单的说,别怕麻烦。即使专家们也不会嫌麻烦。

说明白些,主要问题在于:

n          异常规范会获得令人吃惊的性能得分,例如,编译器会关闭指定异常规范的函数的内联特性。

n          一个运行期的unexpected()错误往往并不是你所期望的、异常规范所能捕获的异常。

n          通常情况下,你无论如何都不可能为函数模板编写出真正有用的异常规范,因为你往往都无法明确函数操作的类型会抛出何种异常。

 

更多内容,可以查看位于http://www.gotw.ca/publications/xc++s/boost_es.htm的Boost异常规范基本原则(内容概括为"Don't!")等文档。

 

参考文献:

[1] H.Sutter.Exceptional C++(Addison-Wesley,2000).

[2] H.Sutter.More Exceptional C++(Addison-Wesley,2002).

本文地址:http://com.8s8s.com/it/it23904.htm