27-7 函数 try 块
try-catch 代码块在大多数情况下都足够有效,但有一种特殊情况它们却不足以解决问题。请看以下示例:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // Exception thrown here
}
};
class B : public A
{
public:
B(int x) : A{x} // A initialized in member initializer list of B
{
// What happens if creation of A fails and we want to handle it here?
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}
在上面的例子中,派生类 B 调用了基类构造函数 A,而 A 可能会抛出异常。由于对象 b 的创建被放在了 try 代码块中(在 main() 函数中),所以如果 A 抛出异常,main 函数的 try 代码块会捕获到它。因此,该程序会输出:

但如果我们想在 B 内部捕获异常呢?对基类构造函数 A 的调用是通过成员初始化列表进行的,这发生在 B 构造函数体被调用之前。因此,无法用标准的 try 代码块将其包裹起来。
在这种情况下,我们需要使用一个稍微修改过的 try 代码块,称为函数 try 代码块。
函数 try 块
函数 try 代码块旨在允许您在整个函数体周围建立异常处理程序,而不是在代码块周围建立异常处理程序。
函数 try 代码块的语法有点难描述,所以我们用例子来说明:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // Exception thrown here
}
};
class B : public A
{
public:
B(int x) try : A{x} // note addition of try keyword here
{
}
catch (...) // note this is at same level of indentation as the function itself
{
// Exceptions from member initializer list or
// from constructor body are caught here
std::cerr << "Exception caught\n";
throw; // rethrow the existing exception
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}
运行此程序后,会产生以下输出:

让我们更详细地分析一下这个程序。
首先,请注意成员初始化列表前添加的关键字try。这表明从该关键字之后的所有内容(直到函数结束)都应该在 try 代码块内进行处理。
其次,请注意,关联的 catch 代码块与整个函数处于同一缩进级别。任何在 try 关键字和函数体结束之间抛出的异常都可以在这里捕获。
当上述程序运行时,变量b开始构造,这会调用 B 的构造函数(该构造函数使用了 try 语句)。B 的构造函数会调用 A 的构造函数,A 的构造函数随后会抛出一个异常。由于 A 的构造函数没有处理这个异常,该异常会沿着调用栈向上传播到 B 的构造函数,并被 B 构造函数的函数级 catch 块捕获。该 catch 块会打印“Exception caught”,然后将当前异常重新抛出到调用栈的上一级,被 B 的 catch 块捕获main(),并打印“Oops”。
最佳实践:
当需要构造函数处理成员初始化列表中抛出的异常时,请使用函数 try 块。
函数 catch 块的限制
对于常规的 catch 块(在函数内部),我们有三种选择:我们可以抛出一个新的异常,重新抛出当前的异常,或者解决异常(通过 return 语句,或者让控制到达 catch 块的末尾)。
构造函数的函数级 catch 块必须抛出一个新的异常或重新抛出现有的异常——它们不允许解析异常!也不允许使用 return 语句,并且到达 catch 块的末尾会隐式地重新抛出异常。
析构函数的函数级 catch 块可以抛出、重新抛出或通过 return 语句解决当前异常。到达 catch 块的末尾将隐式地重新抛出异常。
函数级 catch 代码块可以通过 return 语句抛出、重新抛出或解决当前异常。对于不返回值(void)的函数,到达 catch 代码块的末尾将隐式解决异常;而对于返回值的函数,则会产生未定义行为!
下表总结了函数级 catch 块的局限性和行为:
|---|---|---|
让我们更详细地分析一下这个程序。
首先,请注意成员初始化列表前添加的关键字try。这表明从该关键字之后的所有内容(直到函数结束)都应该在 try 代码块内进行处理。
其次,请注意,关联的 catch 代码块与整个函数处于同一缩进级别。任何在 try 关键字和函数体结束之间抛出的异常都可以在这里捕获。
当上述程序运行时,变量b开始构造,这会调用 B 的构造函数(该构造函数使用了 try 语句)。B 的构造函数会调用 A 的构造函数,A 的构造函数随后会抛出一个异常。由于 A 的构造函数没有处理这个异常,该异常会沿着调用栈向上传播到 B 的构造函数,并被 B 构造函数的函数级 catch 块捕获。该 catch 块会打印“Exception caught”,然后将当前异常重新抛出到调用栈的上一级,被 B 的 catch 块捕获main(),并打印“Oops”。
最佳实践
当需要构造函数处理成员初始化列表中抛出的异常时,请使用函数 try 块。
函数 catch 块的限制
对于常规的 catch 块(在函数内部),我们有三种选择:我们可以抛出一个新的异常,重新抛出当前的异常,或者解决异常(通过 return 语句,或者让控制到达 catch 块的末尾)。
构造函数的函数级 catch 块必须抛出一个新的异常或重新抛出现有的异常——它们不允许解析异常!也不允许使用 return 语句,并且到达 catch 块的末尾会隐式地重新抛出异常。
析构函数的函数级 catch 块可以抛出、重新抛出或通过 return 语句解决当前异常。到达 catch 块的末尾将隐式地重新抛出异常。
函数级 catch 代码块可以通过 return 语句抛出、重新抛出或解决当前异常。对于不返回值(void)的函数,到达 catch 代码块的末尾将隐式解决异常;而对于返回值的函数,则会产生未定义行为!
下表总结了函数级 catch 块的局限性和行为:
| 功能类型 | 可以通过return 语句解决异常 | catch块结束时的行为 |
|---|---|---|
| 构造函数 | 不,必须throw或者rethrow | 隐式rethrow |
| 析构函数 | 是的 | 隐式return |
| 非值返回函数 | 是的 | 解决异常 |
| 返回值的函数 | 是的 | 未定义行为 |
由于 catch 块末尾的这种行为会根据函数的类型而发生巨大变化(对于返回值的函数,还包括未定义行为),我们建议永远不要让控制到达 catch 块的末尾,并且始终显式地抛出异常、重新抛出异常或返回异常。
最佳实践:
避免让控制流到达函数级 catch 代码块的末尾。相反,应该显式地抛出异常、重新抛出异常或返回。
在上面的程序中,如果我们没有在构造函数的函数级 catch 块中显式地重新抛出异常,控制流就会到达函数级 catch 块的末尾,而由于这是一个构造函数,因此会发生隐式重新抛出。结果是一样的。
虽然函数级 try 代码块也可以用于非成员函数,但通常不会这样做,因为这种情况很少见。它们几乎只用于构造函数!
函数 try 代码块可以捕获基础类异常和当前类异常。
在上面的例子中,如果 A 或 B 的构造函数抛出异常,则会被 B 的构造函数周围的 try 代码块捕获。
我们可以在以下示例中看到这一点,其中我们从类 B 而不是类 A 抛出了异常:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
}
};
class B : public A
{
public:
B(int x) try : A{x} // note addition of try keyword here
{
if (x <= 0) // moved this from A to B
throw 1; // and this too
}
catch (...)
{
std::cerr << "Exception caught\n";
// If an exception isn't explicitly thrown here,
// the current exception will be implicitly rethrown
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}

不要使用函数来尝试清理资源
当对象构造失败时,类的析构函数不会被调用。因此,你可能会想用 try 代码块来清理一个在失败前已部分分配资源的类。然而,由于对象在 catch 代码块执行之前就已经“死亡”,因此引用失败对象的成员会被视为未定义行为。这意味着你不能使用 try 代码块来清理类。如果你想清理类,请遵循清理抛出异常的类的标准规则(参见第27.5 课“异常、类和继承”中的“构造函数失败时”小节)。
try 函数主要用于在将异常向上传递之前记录失败,或者用于更改抛出的异常类型。

浙公网安备 33010602011771号