C++ 函数 try 块

常规的 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

详细流程:

  1. 变量 b 开始构造,调用 B 的构造函数(使用函数 try 块)。
  2. B 的构造函数调用 A 的构造函数,A 抛出异常;A 未处理,异常沿栈向上传播。
  3. 异常进入 B 的函数级 catch,输出 “Exception caught”,随后 throw; 重新抛出。
  4. 异常最终被 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 块的主要用途只有两个:

  1. 在向上传递异常前记录失败日志;
  2. 转换抛出的异常类型。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"