异常处理


在〈foreach 与数组〉中使用宏实现了 foreach,这是种创造新语法的概念,然而,由于宏编写与维护不易,C 代码中若使用了宏,也会增加调试的困难,基本上不建议定义过于复杂的宏。

若只是单纯将定义新语法,作为一种挑战,倒也是种乐趣,间接地也可以增进对 C 语言或宏的了解,例如,来试着实现现代高阶语言中的异常处理机制?

C 语言的错误处理,可由函数返回错误码,函数调用方检查错误码来实现,然而,若函数调用链很深,想在底层错误发生时逐一返回,就必须在每层调用时都记得检查错误,例如:

#include <stdio.h>

enum Either { LEFT = 1, RIGHT = 0 };

int b(int p) {
    if(p == 0) {
       return LEFT; 
    }
    puts("do b");
    return RIGHT;
}

int a(int p) {
    if(b(p)) {
       return LEFT; 
    }
    puts("do a");
    return RIGHT;
}

int main(void) {
    if(a(0)) {
        puts("Shit happens!");
        return;
    }

    puts("继续流程");

    return 0;
}

如果想在错误发生时,能够直接跳回最初的函数调用点,能不能做到呢?谈到跳跃会想到goto,不过goto只能在指定同一函数的标签进行跳跃,若想在函数间进行跳跃,要使用 setjmp.h 定义的setjmplongjmp

#define setjmp(env) /* 实现品的定义 */
_Noreturn void longjmp( jmp_buf env, int status );

setjmplongjmp之间必须透过jmp_buf来合作,setjmp本身就是个宏,作用上有点像是设定goto目标标签的概念,只不过是信息是存储在jmp_buf,首次执行setjmp,返回值会是 0。

longjmp则可以对比为goto的概念,_Noreturn是 C11 标准制定的关键字,开发者也可以使用noreturn宏(定义于 stdnoreturn.h),表示函数不会是执行了return或执行至函数底部而结束,编译器可以据此决定是否实行检查。

调用longjmp时指定的jmp_buf,决定了该跳回哪个setjmp调用处,而status决定了回到setjmp调用处时的返回值,例如上面的范例可以修改为:

#include <stdio.h>
#include <setjmp.h>

enum Either { LEFT = 1, RIGHT = 0 };

jmp_buf env;

void b(int p) {
    if(p == 0) {
        longjmp(env, LEFT);
    }
    puts("do b");
}

void a(int p) {
    b(p);
    puts("do a");
}

int main(void) {
    switch(setjmp(env)) {
        case RIGHT:
            a(0);
            puts("继续流程");
            break;
        case LEFT:
            puts("Shit happens!");
    }

    return 0;
}

因为status决定了回到setjmp调用处时的返回值,也就可以区分不同的错误:

#include <stdio.h>
#include <setjmp.h>

enum Status { OK, ZERO_ERR, OUT_OF_RANGE };

jmp_buf env;

void b(int p) {
    if(p == 0) {
        longjmp(env, ZERO_ERR);
    }
    else if(p > 100) {
        longjmp(env, OUT_OF_RANGE);
    }
    puts("do b");
}

void a(int p) {
    b(p);
    puts("do a");
}

int main(void) {
    switch(setjmp(env)) {
        case OK: 
            a(101);
            puts("继续流程");
            break;
        case ZERO_ERR:
            puts("不能为 0");
            break;
        case OUT_OF_RANGE:
            puts("不能超过 100");
    }

    return 0;
}

若单纯根据这个简单流程作为基础,可以初步定义以下宏来取代:

#include <stdio.h>
#include <setjmp.h>

#define try switch(setjmp(env)) { case OK:
#define catch(x) break; case x:
#define throw(x) longjmp(env, x)

enum Status { OK, ZERO_ERR, OUT_OF_RANGE };

jmp_buf env;

void b(int p) {
    if(p == 0) {
        throw(ZERO_ERR);
    }
    else if(p > 100) {
        throw(OUT_OF_RANGE);
    }
    puts("do b");
}

void a(int p) {
    b(p);
    puts("do a");
}

int main(void) {
    try
        a(111);
        puts("继续流程");
    catch(ZERO_ERR) 
        puts("不能为 0");
    catch(OUT_OF_RANGE) 
        puts("不能超过 100");
    } // 这边的 } 怎么办?

    return 0;
}

只是用宏单纯地展开为原始代码罢了,范例中孤独的}感觉很怪,那就用个 finally 来取代好了:

#include <stdio.h>
#include <setjmp.h>

#define try switch(setjmp(env)) { case OK:
#define catch(x) break; case x:
#define finally }
#define throw(x) longjmp(env, x)

enum Status { OK, ZERO_ERR, OUT_OF_RANGE };

jmp_buf env;

void b(int p) {
    if(p == 0) {
        throw(ZERO_ERR);
    }
    else if(p > 100) {
        throw(OUT_OF_RANGE);
    }
    puts("do b");
}

void a(int p) {
    b(p);
    puts("do a");
}

int main(void) {
    try {
        a(111);
        puts("继续流程");
    }
    catch(ZERO_ERR) {
        puts("不能为 0");
    }
    catch(OUT_OF_RANGE) {
        puts("不能超过 100");
    }
    finally {
        puts("一定要做的…");
    }

    return 0;
}

如果没有最后一定要做的事情,那么只写个finally不要定义区块,阅读上也代表着没做什么事,这个宏也可以结合{}来区别trycatch,不过语法上有个小问题,这样写也可以:

try {
    a(1);
    puts("继续流程");
}
catch(ZERO_ERR) {
    puts("不能为 0");
}
catch(OUT_OF_RANGE) {
    puts("不能超过 100");
}}

如果想强制try一定得与finally匹配,可以使用do/while,因为dowhile正好也是一对:

#include <stdio.h>
#include <setjmp.h>

#define try do { switch(setjmp(env)) { case OK:
#define catch(x) break; case x:
#define finally }} while(0);
#define throw(x) longjmp(env, x)

enum Status { OK, ZERO_ERR, OUT_OF_RANGE };

jmp_buf env;

void b(int p) {
    if(p == 0) {
        throw(ZERO_ERR);
    }
    else if(p > 100) {
        throw(OUT_OF_RANGE);
    }
    puts("do b");
}

void a(int p) {
    b(p);
    puts("do a");
}

int main(void) {
    try {
        a(1);
        puts("继续流程");
    }
    catch(ZERO_ERR) {
        puts("不能为 0");
    }
    catch(OUT_OF_RANGE) {
        puts("不能超过 100");
    } finally

    return 0;
}

这个范例只是个简单探讨,看看宏创造语法的可能性,开发者创建宏应该避免复杂,最好是基于既有代码,若发现流程具有固定模式,使用函数封装流程不易或者无法以函数封装时,再来考虑可否以宏取代,而非凭空创造。


展开阅读全文