C++基础教程面向对象(学习笔记(78))

重新抛出异常

有时候,您可能会遇到想要捕获异常的情况,但不希望(或有能力)在捕获它时完全处理它。这在您想要记录错误,这很很常见,但会将问题传递给调用者以实际处理。

当函数可以使用返回代码时,这很简单。请考虑以下示例:

Database* createDatabase(std::string filename)
{
    try
    {
        Database *d = new Database(filename);
        d->open(); // 假设这会在失败时抛出一个int异常
        return d;
    }
    catch (int exception)
    {
        // 数据库创建失败
        //将错误写入某个全局日志文件
        g_log.logError("Creation of Database failed");
    }
 
    return nullptr;
}

在上面的代码片段中,该函数的任务是创建Database对象,打开数据库并返回Database对象。在出现问题的情况下(例如传入错误的文件名),异常处理程序会记录错误,然后合理地返回空指针。

现在考虑以下功能:

int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
    assert(d);
 
    try
    {
        return d->getIntValue(table, key); // 失败时抛出int异常
    }
    catch (int exception)
    {
        // 失败时抛出int异常
        g_log.logError("doSomethingImportant failed");
 
        // 但是,我们实际上没有处理此错误
        // 那么我们在这做什么?
    }
}

在此函数成功的情况下,它返回一个整数值 - 任何整数值都可以是有效值。

但是getIntValue()出现问题的情况呢?在这种情况下,getIntValue()将抛出一个整数异常,该异常将由getIntValueFromDatabase()中的catch块捕获,该异常将记录错误。但那么我们如何告诉调用者getIntValueFromDatabase()出错了什么?与顶部示例不同,我们在这里没有一个好的返回码(因为任何整数返回值都可能是有效的)。

抛出一个新的异常

一个明显的解决方案是抛出一个新的异常。

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("doSomethingImportant failed");
 
        throw 'q'; // 将char异常'q'抛出堆栈,由getIntValueFromDatabase()的调用者处理
    }
}

在上面的示例中,程序从getIntValue()捕获int异常,记录错误,然后抛出char值为“q”的新异常。虽然从catch块中抛出异常似乎很奇怪,但这是允许的。请记住,只有try块中抛出的异常才有资格被捕获。这意味着catch块中抛出的异常不会被它所在的catch块捕获。相反,它将在堆栈中传播给调用者。

catch块抛出的异常可以是任何类型的异常 - 它不需要与刚刚捕获的异常类型相同。

重新抛出异常(错误的方式)

另一种选择是重新抛出相同的异常。一种方法是这样做:

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("doSomethingImportant failed");
 
        throw exception;
    }
}

虽然这有效,但这种方法有一些缺点。首先,这不会抛出与捕获的异常完全相同的异常 - 而是抛出变量异常的复制初始化副本。尽管编译器可以自由地删除副本,但它可能不会,因此这可能会降低性能。

但重要的是,考虑以下情况会发生什么:

int getIntValueFromDatabase(Database *d, std::string table, std::string key)
{
    assert(d);
 
    try
    {
        return d->getIntValue(table, key); // 失败时抛出派生异常
    }
    catch (Base &exception)
    {
        // 将错误写入某个全局日志文件
        g_log.logError("doSomethingImportant failed");
 
        throw exception; // 危险:这会抛出一个Base对象,而不是Derived对象
    }
}

在这种情况下,getIntValue()抛出Derived对象,但catch块正在捕获Base引用。这很好,因为我们知道我们可以对Derived对象有一个Base引用。但是,当我们抛出异常时,抛出的异常是从变量异常中复制初始化的。变量异常具有类型Base,因此复制初始化异常也具有类型Base(不是Derived!)。换句话说,我们的Derived()对象已被切片!

您可以在以下程序中看到:

#include <iostream>
class Base
{
public:
    Base() {}
    virtual void print() { std::cout << "Base"; }
};
 
class Derived: public Base
{
public:
    Derived() {}
    virtual void print() { std::cout << "Derived"; }
};
 
int main()
{
    try
    {
        try
        {
            throw Derived();
        }
        catch (Base& b)
        {
            std::cout << "Caught Base b, which is actually a ";
            b.print();
            std::cout << "\n";
            throw b; // the Derived object gets sliced here
        }
    }
    catch (Base& b)
    {
        std::cout << "Caught Base b, which is actually a ";
        b.print();
        std::cout << "\n";
    }
 
    return 0;
}

这打印:

Caught Base b, which is actually a Derived
Caught Base b, which is actually a Base

第二行表明Base实际上是Base而不是Derived的事实证明Derived对象是切片的。

重新抛出异常(正确的方式)

幸运的是,C ++提供了一种方法来重新抛出与刚刚捕获的异常完全相同的异常。为此,只需在catch块中使用throw关键字(没有关联的变量),如下所示:

#include <iostream>
class Base
{
public:
    Base() {}
    virtual void print() { std::cout << "Base"; }
};
 
class Derived: public Base
{
public:
    Derived() {}
    virtual void print() { std::cout << "Derived"; }
};
 
int main()
{
    try
    {
        try
        {
            throw Derived();
        }
        catch (Base& b)
        {
            std::cout << "Caught Base b, which is actually a ";
            b.print();
            std::cout << "\n";
            throw; // 注意:我们现在正在重新抛出这个对象
        }
    }
    catch (Base& b)
    {
        std::cout << "Caught Base b, which is actually a ";
        b.print();
        std::cout << "\n";
    }
 
    return 0;
}

这打印:

Caught Base b, which is actually a Derived
Caught Base b, which is actually a Derived

这个throw关键字似乎没有特别抛出任何东西,实际上会重新抛出刚捕获的完全相同的异常。没有制作副本,这意味着我们不必担心性能下降副本或切片。

如果需要重新抛出异常,则此方法应优先于替代方法。

规则:当重新抛出相同的异常时,请单独使用throw关键字。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章