C++异常处理新姿势:再抛出异常的妙用

1,354 阅读4分钟

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」mp.weixin.qq.com/s/IkSvO8bP0…

_4f37f7c8-b9e9-4174-bebd-eb74dbf35d08.jpeg

这是《掌握 C++ 异常艺术:构建健壮程序的秘诀与实战策略》系列文章的第四篇,关注我可以查看系列里其它文章。

有些场景下,需要重新抛出当前捕捉到的异常信号,有个比较便捷的语法:

throw;

在关键词 throw 之后不跟任何的异常信号,就表示再次抛出当前已捕获的异常信号。

这种写法真的有点讨巧,不过基于这样的语法,可以方便我们实现两方面的特性功能:

简单的堆栈跟踪

为了方便调试和后期排查问题的起因,问题发生后都希望能查看异常发生时的程序执行路径,包括从程序的入口点到当前位置(通常是异常发生的地方)。

而堆栈跟踪就记录了特定时刻的调用栈状态,那么如何利用异常的再抛出便利,来简单实现堆栈跟踪的类似形式?看下面的例子:

class MyException
    : public std::exception
{
public:
    MyException()
      : std::exception(),
      str("MyException ") { }

    virtual const char* what()
        const noexcept override {
        return str.c_str();
    }

    void addInfo(const std::string& info) {
        str += info;
    }

private:
    std::string str;
};

void g()
{
    try {
        // ...
        throw MyException();
    } catch (MyException& e) {
        std::stringstream ss;
        ss << __func__ << "() failed\n";
        e.addInfo(ss.str().c_str());
        throw;
    }
}

void f()
{
    try {
        g();
    } catch (MyException& e) {
        std::stringstream ss;
        ss << __func__ << "() failed\n";
        e.addInfo(ss.str().c_str());
        throw;
    }
}

int main()
{
    try {
        f();
    } catch (const std::exception& e) {
        std::cerr << "catch err: "
            << e.what() << std::endl;
    }
    return 0;
}

代码中,基于异常类 std::exception 派生一个子类 MyException,然后实现接口 addInfo,用于在原有异常信息基础上添加新的异常信息。

调用会抛出 MyException 异常信号的函数 f() (间接抛出是为了演示多层调用栈的展开)后,捕捉(catch)异常信号,捕获成功则调用异常信号的 addInfo 接口用于添加当前调用栈的信息。

__func__ 是预定义的内置标识符,代表当前函数名。

然后利用 throw; 再次抛出异常信号,这时异常信号已经包含当前调用栈的信息了,因而后续调用栈展开的过程中,随时都可以获取异常发生后已被展开的各级调用栈的相关信息。

程序跑起来,看看效果:

catch err: MyException g() failed
f() failed

异常分发器

代码中业务众多,需要考虑的异常处理自然也多,如果处理比较分散,就会产生多而杂的问题,那么如何简化异常处理呢?

将捕获后的异常集中起来处理,那么管理维护就变得方便简单,这需要实现类似调度器的功能,这里姑且叫它「异常分发器」吧。

下面举个小栗子:

void handleException()
{
    try {
        throw;
    } catch (const MyException1& e) {
        // ...code to handle MyException1...
    } catch (const MyException2& e) {
        // ...code to handle MyException2...
    }
    // ...
}

void f()
{
    try {
        // ...something that might throw...
    } catch (...) {
        handleException();
    }
}

void g()
{
    try {
        // ...something that might throw...
    } catch (...) {
        handleException();
    }
}

上面的代码里,handleException() 函数负责统一处理异常信号。当在 f() 函数中捕获到异常时,会调用 handleException() 函数来处理异常。

但是,如果需要 handleException() 函数来处理已捕获的异常信号,那么是不是就要往这函数参数传递信号?

handleException() 函数中的 try 块会利用 throw; 重新抛出当前已捕获的异常信号,就算不知道当前已捕获的异常信号具体是什么类型也无所谓,然后 catch 块再次匹配捕捉不同的异常类型进行处理,这样调用 handleException() 函数时,可免除将异常信号作为参数的传递,也就不用考虑需要兼容各种类型异常信号的麻烦。

实现异常分发器使得异常信号的处理逻辑可以高度集中,提高了代码的复用性和可维护性。


使用 throw; 重新抛出当前异常信号,使得异常处理变得更加灵活和高效。无论是为了调试还是提高代码的健壮性,这种写法都提供了很大的帮助。在实际项目中,运用好这种技巧,可以显著提升代码质量和调试效率。

各位看官可以再发散一下思维,或许有用的地方不止这两种。