函数try块
本节阅读量:try和catch块在大多数情况下都工作得很好,但有一种特殊情况它们并不够用。考虑以下示例:
|
|
在上面的示例中,派生类B调用基类构造函数A,而该构造函数可能抛出异常。由于对象b的创建已经放在try块中(在函数main()中),因此如果A抛出异常,main的try块将捕获它。因此,该程序会打印:
|
|
但如果我们想在B内部捕获异常呢?在调用B构造函数的函数体之前,基类构造函数A会通过成员初始化列表被调用。我们没有办法在它周围设置普通的try块。
在这种情况下,我们必须使用稍微修改的try块,称为函数try块。
函数try块
函数try块的设计目的,是允许您围绕整个函数体建立异常处理程序,而不是只围绕某个代码块。
函数try块的语法有点难以描述,因此我们通过示例演示:
|
|
运行此程序时,它会产生以下输出:
|
|
让我们更详细地检查这个程序。
首先,注意在成员初始化列表之前添加了try关键字。这表示该点之后的所有内容(直到函数结束)都应被视为位于try块内。
其次,请注意,关联的catch块与整个函数处于相同的缩进级别。在try关键字和函数体末尾之间引发的任何异常,都可以在此处捕获。
当上面的程序运行时,变量b开始构造,并调用B的构造函数(使用函数try块)。B的构造函数调用A的构造函数,然后引发异常。由于A的构造函数不处理该异常,因此异常向上传播到B的构造函数,并在那里被B构造函数的函数级catch捕获。catch块打印“Exception caught”,然后重新抛出当前异常,该异常由main()中的catch块捕获,后者打印“Oops”。
最佳实践
当需要构造函数来处理成员初始值设定项列表中引发的异常时,请使用函数try块。
函数catch块的限制
对于常规catch块(位于函数内),我们有三个选项:可以抛出新的异常、重新抛出当前异常,或解决异常(通过return语句,或让控制流执行到catch块末尾)。
构造函数的函数级catch块必须抛出新异常或当前异常——不允许它们解决异常!也不允许return语句,到达catch块末尾会隐式重新抛出异常。
析构函数的函数级catch块可以抛出新异常或当前异常,也可以通过return语句解决异常。到达catch块末尾会隐式重新抛出异常。
其他函数的函数级catch块可以抛出新异常或当前异常,也可以通过return语句解决异常。到达catch块末尾会隐式解决返回void函数的异常;对于其他返回类型的函数,则会产生未定义行为!
下表总结了函数级捕获块的限制和行为:
| 函数类型 | 是否可以使用return语句解决异常 | 执行到catch块结束的行为 |
|---|---|---|
| 构造函数 | 否 | 隐式重新抛出异常 |
| 析构函数 | 是 | 隐式重新抛出异常 |
| 返回void的函数 | 是 | 解决异常 |
| 返回非void的函数 | 是 | 未定义的行为 |
由于到达catch块末尾时的行为会因函数类型而异(并且在返回非void类型函数的情况下包含未定义行为),因此我们建议不要让控制流到达catch块末尾,而是始终显式抛出、重新抛出异常,或使用return语句。
在上面的程序中,如果我们没有在构造函数的函数级catch块中显式重新抛出异常,控制流就会到达函数级catch块末尾。由于这是构造函数,因此会发生隐式重新抛出。结果是一样的。
尽管函数级try块也可以与非成员函数一起使用,但通常没人这样做,因为很少有这种需求。它们几乎专用于构造函数!
最佳实践
避免让控制流到达函数级catch块的末尾。请显式抛出、重新抛出异常或显式return。
函数try块可以捕获基类异常和当前类异常
在上面的例子中,如果A或B的构造函数抛出异常,它都会被B构造函数周围的try块捕获。
在下面的示例中,我们可以看到这一点,其中异常从类B而不是类A中引发:
|
|
我们得到相同的输出:
|
|
不要使用函数try块清理资源
当对象的构造失败时,不会调用类的析构函数。因此,您可能会尝试使用函数try块清理失败前已经分配的部分资源。然而,引用构造失败对象的成员被认为是未定义行为,因此对象在catch块执行之前已经是“死的”。这意味着您不能在类构造失败之后使用函数try块尝试清理。如果要在类构造失败后完成清理,请遵循处理抛出异常类的标准规则(RAII)。
函数try块主要用于在将异常传递到调用栈之前记录失败,或者用于更改抛出的异常类型。