在〈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 定義的 setjmp
與 longjmp
:
#define setjmp(env) /* 實作品的定義 */
_Noreturn void longjmp( jmp_buf env, int status );
setjmp
與 longjmp
之間必須透過 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
不要定義區塊,閱讀上也代表著沒做什麼事,這個巨集也可以結合 {}
來區別 try
、catch
,不過語法上有個小問題,這樣寫也可以:
try {
a(1);
puts("繼續流程");
}
catch(ZERO_ERR) {
puts("不能為 0");
}
catch(OUT_OF_RANGE) {
puts("不能超過 100");
}}
如果想強制 try
一定得與 finally
匹配,可以使用 do/while
,因為 do
與 while
正好也是一對:
#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;
}
這個範例只是個簡單探討,看看巨集創造語法的可能性,開發者建立巨集應該避免複雜,最好是基於既有程式碼,若發現流程具有固定模式,使用函式封裝流程不易或者無法以函式封裝時,再來考慮可否以巨集取代,而非憑空創造。