常规的 try 与 catch 块在多数场景下已足够,但在一种特殊情况下却力有未逮。请看以下示例:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // 此处抛出异常
}
};
class B : public A
{
public:
B(int x) : A{x} // 在 B 的成员初始化列表中构造 A
{
// 若 A 的构造失败,而我们又想在此处处理,该怎么办?
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}
在上述代码中,派生类 B 调用了基类 A 的构造函数,后者可能抛出异常。由于对象 b 的创建被置于 main 的 try 块内,一旦 A 抛出异常,main 的 try 块会捕获它,于是程序输出:
Oops
然而,如果我们希望在 B 内部捕获该异常,则无法实现:调用基类构造函数发生在成员初始化列表阶段,早于 B 构造函数体执行,常规的 try 块无法将其包围。
此时,我们需要使用一种稍作变形的 try 块——函数 try 块(function try block)。
函数 try 块
函数 try 块旨在为整个函数体设置异常处理器,而不仅仅是某段代码。
其语法较特殊,最佳方式是通过示例说明:
#include <iostream>
class A
{
private:
int m_x;
public:
A(int x) : m_x{x}
{
if (x <= 0)
throw 1; // 此处抛出异常
}
};
class B : public A
{
public:
B(int x) try : A{x} // 注意:在成员初始化列表前添加 try
{
}
catch (...) // catch 块与函数本身保持同一缩进级别
{
// 捕获成员初始化列表或构造函数体中抛出的异常
std::cerr << "Exception caught\n";
throw; // 重新抛出当前异常
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}
运行结果:
Exception caught
Oops
详细流程:
- 变量 b 开始构造,调用 B 的构造函数(使用函数 try 块)。
- B 的构造函数调用 A 的构造函数,A 抛出异常;A 未处理,异常沿栈向上传播。
- 异常进入 B 的函数级 catch,输出 “Exception caught”,随后
throw;
重新抛出。 - 异常最终被 main 的 catch 捕获,输出 “Oops”。
最佳实践
当需要在构造函数内处理成员初始化列表抛出的异常时,应使用函数 try 块。
函数 catch 块的限制
对于普通(函数内)catch 块,有三种出路:
- 抛出新异常;
- 重新抛出当前异常;
- 通过 return 语句或正常结束 catch 块来“解决”异常。
对于构造函数和析构函数,规则有所不同,总结如下:
函数类型 | 能否通过 return 解决异常 | catch 块末尾的默认行为 |
---|---|---|
构造函数 | 否,必须 throw 或 rethrow | 隐式 rethrow |
析构函数 | 是 | 隐式 rethrow |
无返回值函数 | 是 | 解决异常 |
有返回值函数 | 是 | 未定义行为 |
由于默认行为随函数类型而变化,且在返回值函数中可能导致未定义行为,建议始终显式 throw、rethrow 或 return,不要让控制流自然到达 catch 块末尾。
最佳实践
避免让控制流隐式结束函数级 catch 块;务必显式 throw、rethrow 或 return。
虽然函数 try 块也可用于非成员函数,但实际极少需要;它们几乎专用于构造函数。
函数 try 块可同时捕获基类及当前类异常
在上例中,无论 A 还是 B 的构造函数抛出异常,都会被 B 构造函数的 try 块捕获:
#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}
{
if (x <= 0) // 将原本位于 A 的检查移至 B
throw 1; // 此处抛出异常
}
catch (...)
{
std::cerr << "Exception caught\n";
// 若此处不显式抛出,当前异常将隐式 rethrow
}
};
int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}
输出:
Exception caught
Oops
不要用函数 try 块来清理资源
对象构造失败时,析构函数不会被调用。因此,你可能会想用函数 try 块来清理已部分分配的资源。然而,在 catch 块中访问已失败对象的成员属于未定义行为,因为对象在 catch 执行前已“死亡”。因此,函数 try 块不能用于类内资源清理。如需在构造失败时清理资源,请遵循 《异常、类与继承》中“构造函数失败”一节给出的标准做法。
函数 try 块的主要用途只有两个:
- 在向上传递异常前记录失败日志;
- 转换抛出的异常类型。