例外處理


在〈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;
}

這個範例只是個簡單探討,看看巨集創造語法的可能性,開發者建立巨集應該避免複雜,最好是基於既有程式碼,若發現流程具有固定模式,使用函式封裝流程不易或者無法以函式封裝時,再來考慮可否以巨集取代,而非憑空創造。