C/C++ 中的异常处理

setjmp in C

在 Mac OS 开发环境下,man setjmp 可以看到setjmp函数簇的定义和说明:

#include <setjmp.h>

void _longjmp(jmp_buf env, int val);
int _setjmp(jmp_buf env);

void longjmp(jmp_buf env, int val);
void longjmperror(void);
int setjmp(jmp_buf env);

void siglongjmp(sigjmp_buf env, int val);
int sigsetjmp(sigjmp_buf env, int savemask);

DESCRIPTION

The sigsetjmp(), setjmp(), and _setjmp() functions save their calling environment in env. Each of these functions returns 0. The corresponding longjmp() functions restore the environment saved by their most recent respective invocations of the setjmp() function. They then return, so that program execution continues as if the corresponding invocation of the setjmp() call had just returned the value specified by val, instead of 0. Pairs of calls may be intermixed (i.e., both sigsetjmp() and siglongjmp() and setjmp() and longjmp() combinations may be used in the same program); however,individual calls may not (e.g. the env argument to setjmp() may not be passed to siglongjmp()). The longjmp() routines may not be called after the routine which called the setjmp() routines returns. All accessible objects have values as of the time longjmp() routine was called, except that the values of objects of automatic storage invocation durationthat do not have the volatile type and have been changed between the setjmp() invocation and longjmp() call are indeterminate. The setjmp()/longjmp() pairs save and restore the signal mask while _setjmp()/_longjmp() pairs save and restore only the register set and the stack. (Seesigprocmask(2).) The sigsetjmp()/siglongjmp() function pairs save and restore the signal mask if the argument savemask is non-zero; otherwise, only the register set and thestack are saved.

从上面的说明可以看出:_setjmp 和 _longjmp 这一对函数通过恢复寄存器和堆栈,来实现一个non-local的跳转.

setjmp 和 longjmp 函数对恢复 signal mask, sigsetjmp 和 siglongjmp 函数根据 savemask 参数来决定恢复的内容.

注:为了表述方便,下文用 setjmp 来代表 setjmp 函数簇,包括上述三个函数对。

MSDN 中说得更清晰一些:

setjmp 函数用于保存程序的运行时的堆栈环境,接下来的其它地方,你可以通过调用 longjmp 函数来恢复先前被保存的程序堆栈环境。

当 setjmp 和 longjmp 组合一起使用时,它们能提供一种在程序中实现“非本地局部跳转”("non-local goto")的机制。

并且这种机制常常被用于来实现,把程序的控制流传递到错误处理模块之中;或者程序中不采用正常的返回(return)语句,或函数的正常调用等方法,而使程序能被恢复到先前的一个调用例程(也即函数)中。

比较有意思的是,setjmp 函数的函数返回值有两个:

  • 第一次返回在设置的时候(设置 jmp_buf 的初值,这实际上是一个数组,保存了寄存器的当前值), 返回0表示成功.
  • 第二次返回在 longjmp 调用的时候,返回的是 longjmp 的调用参数。

setjmp 使用的陷阱:

  1. longjmp 必须在 setjmp 之后调用,否则结局难料。
  2. 不要把它当作全局的 goto 来使用,它唯一应该做的事情,应该是异常处理。
  3. 在 C++ 中杜绝使用,因为它无法完美的支持 OO 的语义,与 RAII 的思想也冲突。
  4. setjmp 不应该封装在函数中,因为如果这样,longjmp 返回的时候,堆栈已经失效,会导致运行时异常,如果需要封装,应该使用宏。

第 3 点决定了 setjmp 在绝大部分的工程环境下,都是不适合使用的

一个使用的例子:

#include <stdio.h>
#include <setjmp.h>
jmp_buf buf;
void func1()
{
    printf("func1() throws exception.\n");
    longjmp(buf, 1);
}
void func2()
{
    printf("func2() throws exception.\n");
    longjmp(buf, 2);
}
int main()
{
    int ret = setjmp(buf);
    if (ret == 0) {
        func1();
        func2();
    } else if (ret == 1) {
        printf("catch exception from func1().\n");
    } else if (ret == 2) {
        printf("catch exception from func2().\n");
    } else {
        printf("catch unexpected exception.\n");
    }
    return 0;
}

try-catch in C++

C++ 提供了 try-catch 的语法来处理异常,比起 C 的 setjmp 来显得优雅了许多:拥有不错的运行时效率的同时,能够很好的恢复堆栈处理异常。

使用的陷阱:

  1. 不要与 goto、setjmp 等 C 风格的跳转语句一起使用。
  2. 不要在构造函数中抛出异常,这会导致一个未完成的对象,将无法调用析构函数。
  3. C++ 不支持异常的异常,所以如果在抛出异常的回退中,析构函数中再次抛出异常,会导致错误,中止程序。

个人的意见:一般在简单的代码中,返回值足以保证正常的运作的情况下,没有必要用 try-catch,它毕竟会带来程序的开销(在编译期给每个包含 try 的函数块定义一个 try 表,纪录位置信息和对应的 catch 信息)。而且对 exception 对象的管理,比错误码来的复杂的多,KISS 就好。

不过 C++ 中对异常处理的实现还是比较有意思的,具体的可以参考这篇文章

参考文章