C++ 重新抛出异常

有时你会遇到这样一种情况:想捕获某个异常,但并不打算(或无法)在当前位置彻底处理它。最常见的场景是记录错误日志后,再把问题交给更上层的调用者去处理。

当函数能够使用返回码时,这一点很容易做到。例如:

Database* createDatabase(std::string filename)
{
    Database* d{};

    try
    {
        d = new Database{};
        d->open(filename);        // 假设失败时抛出 int 异常
        return d;
    }
    catch (int exception)
    {
        // 数据库创建失败
        delete d;
        g_log.logError("Creation of Database failed");
    }

    return nullptr;
}

在上面的代码中,函数负责创建并打开一个 Database 对象。一旦出错(如传入错误文件名),异常处理器先记录日志,然后返回空指针,将错误信息反馈给调用者。

现在考虑另一个函数:

int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
    assert(d);

    try
    {
        return d->getIntValue(table, key); // 失败时抛出 int 异常
    }
    catch (int exception)
    {
        g_log.logError("getIntValueFromDatabase failed");

        // 然而错误尚未真正处理
        // 那这里应该做什么?
    }
}

若函数执行成功,它返回一个整数值——任何整数值都可能是合法结果。

一旦 getIntValue() 出错,它会抛出 int 异常,被 getIntValueFromDatabase() 捕获并记录日志。但随后如何通知调用者发生了错误?与前面的例子不同,这里无法通过返回码表达错误,因为任何 int 值都可能是合法数据。

抛出新的异常

一种显而易见的解决方案是抛出一个新的异常

int getIntValueFromDatabase(Database* d, std::string table, std::string key)
{
    assert(d);

    try
    {
        return d->getIntValue(table, key);
    }
    catch (int exception)
    {
        g_log.logError("getIntValueFromDatabase failed");

        throw 'q'; // 向调用者抛出 char 异常
    }
}

上述代码捕获到 int 异常后记录日志,再抛出一个 char 类型的异常。虽然看起来奇怪,但在 catch 块内抛出异常是允许的。记住:只有 try 块内抛出的异常才会被该 try 块捕获。因此,catch 块内抛出的异常不会再次被自身捕获,而是继续向调用栈上层传播。

此外,catch 块内抛出的异常类型可以与刚捕获的异常类型不同。

重新抛出异常(错误做法)

另一种选择是重新抛出同一个异常。一种常见但不够理想的做法如下:

catch (int exception)
{
    g_log.logError("getIntValueFromDatabase failed");
    throw exception; // 抛出 exception 的副本
}

虽然可行,但存在两个问题:

  1. 性能:抛出的并非原异常对象,而是变量 exception 的副本。尽管编译器可以省略复制,但也可能不会,因此可能存在额外开销。
  2. 对象切片:考虑以下场景:
catch (Base& exception)
{
    ...
    throw exception; // 危险!只会抛出 Base 对象,而非 Derived 对象
}

如果实际抛出的是 Derived 对象,而 catch 捕获的是 Base&,则 throw exception; 只会复制 Base 部分,导致对象切片。

验证程序:

#include <iostream>

class Base
{
public:
    Base() {}
    virtual void print() { std::cout << "Base"; }
};

class Derived : public Base
{
public:
    Derived() {}
    void print() override { std::cout << "Derived"; }
};

int main()
{
    try
    {
        try
        {
            throw Derived{};
        }
        catch (Base& b)
        {
            std::cout << "Caught Base&, actually a ";
            b.print();
            std::cout << '\n';
            throw b; // 发生切片
        }
    }
    catch (Base& b)
    {
        std::cout << "Caught Base&, actually a ";
        b.print();
        std::cout << '\n';
    }
    return 0;
}

输出:

Caught Base&, actually a Derived
Caught Base&, actually a Base

第二行输出“Base”而非“Derived”,证明发生了对象切片。

重新抛出异常(正确做法)

C++ 提供了无操作数 throw 来重新抛出完全相同的异常对象

catch (Base& b)
{
    ...
    throw; // 重新抛出原异常,无复制,无切片
}

示例验证:

#include <iostream>

class Base
{
public:
    Base() {}
    virtual void print() { std::cout << "Base"; }
};

class Derived : public Base
{
public:
    Derived() {}
    void print() override { std::cout << "Derived"; }
};

int main()
{
    try
    {
        try
        {
            throw Derived{};
        }
        catch (Base& b)
        {
            std::cout << "Caught Base&, actually a ";
            b.print();
            std::cout << '\n';
            throw; // 无操作数 throw
        }
    }
    catch (Base& b)
    {
        std::cout << "Caught Base&, actually a ";
        b.print();
        std::cout << '\n';
    }
    return 0;
}

输出:

Caught Base&, actually a Derived
Caught Base&, actually a Derived

无操作数 throw 会重新抛出刚刚捕获的完全相同的异常对象,不会产生任何副本,因此既避免了性能损耗,也避免了对象切片。

规则
如需重新抛出同一异常,应使用无操作数的 throw;

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

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

公众号二维码

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