HOWTO · C

C での Try-Catch

この記事では、C で try-catch 動作を実装する例を示します。これは、デフォルトでは、try-catch/exception 処理メカニズムをサポートしていません。

このページの内容

Cには組み込みのtry-catchや例外処理がありません。<setjmp.h>のsetjmpとlongjmpを使うと、保存した実行位置へ制御を移し、TRY、CATCH、THROWを使った小規模なマクロパターンを構成できます。ただし、これは制御フローを扱う限定的な手法であり、完全な例外システムではありません。リソースは自動解放されず、示したswitchの設計では、対応するCATCHがない値に対してハンドラーは選択されません。各TRYの呼び出しは独自のブロックスコープのbuf_stateとswitchを持つため、このマクロ構成では入れ子の呼び出しも可能です。

CでのTry-Catch

setjmpは実行環境をjmp_bufに保存します。直接呼び出すと戻り値は0です。その後、その実行環境へlongjmpで戻ると、指定した値が戻り値になります(ただし、指定値が0の場合は1として返ります)。longjmpは保存された実行コンテキストを復元し、setjmpの呼び出し位置から実行を再開します。状態をプロセッサへコピーするわけではありません。

以下のマクロはswitchを使います。最初のsetjmpの戻り値によってTRY本体を実行し、各CATCH(value)が対応するcaseを追加します。THROW(value)はlongjmpを呼び出します。対応するものがない非ゼロ値の場合、catch本体は選択されず、switchの終了時にその値は処理されません。TRY本体へ戻されることもありません。各CATCHが生成するbreakにより、後続のcaseへ処理が流れ込むのを防ぎます。

この例を実行してください。

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

#define TRY do { jmp_buf buf_state; switch (setjmp(buf_state)) { case 0:
#define CATCH(value) break; case value:
#define ENDTRY } } while (0)
#define THROW(value) longjmp(buf_state, value)
#define CODE_ONE 1
#define CODE_TWO 2

int main(void) {
  TRY {
    puts("Starting work");
    THROW(CODE_TWO);
    puts("This line is skipped");
  }
  CATCH(CODE_ONE) { puts("Code one"); }
  CATCH(CODE_TWO) { puts("Code two"); }
  ENDTRY;
  return 0;
}
Starting work
Code two

提示された実行例はステータス0で終了しました。この完全なプログラムは、対応するハンドラーへの制御移行を1回示しています。longjmpを呼び出す時点で、jmp_bufが有効でなければなりません。対応するsetjmpを含む関数が戻った後にジャンプすることはできません。また、setjmpを含む関数内の非volatileな自動変数が、setjmpの後からlongjmpの前までに変更されていた場合、その値は不定になることがあります。こうした制御フローと有効期間の規則を理解したうえで、このパターンを使ってください。

CでTry-CatchにFinallyを追加する

CにはFINALLY文はありませんが、do { ... } while (0)のラッパー内でswitchの後に処理を置けば、finallyに似た処理を記述できます。この簡潔な制御フローでは、Duff’s Device風の構成と同じように、ループ内にcaseラベルを置きます。

int status = 0; do { switch (setjmp(env)) { case 0: work(); break; case CODE_TWO: handle(); break; default: status = 1; break; } } while (0); cleanup(); return status;

正常終了時はwork()が完了し、そのbreakでswitchを抜けます。1回だけ実行されるループが終わると、cleanup()へ進みます。対応する例外値を投げた場合は、longjmpによってsetjmpがCODE_TWOを返すため、handle()が実行され、そのbreakから同じクリーンアップ経路へ進みます。対応しない非ゼロ値がsetjmpから返った場合は、defaultが実行され、対応しない結果を記録します。そのbreakもクリーンアップへ進みます。いずれの場合も、breakが抜けるのはswitchであり、関数全体ではありません。これは制御フローの説明例であり、リソースの自動解放や完全な例外処理を実現するものではありません。

以下の実行可能な例では、switchと共通のクリーンアップラベルを使い、対応する例外値を投げたときの経路を示します。

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

#define CODE_TWO 2

int main(void) {
  jmp_buf env;
  switch (setjmp(env)) {
    case 0:
      puts("Starting work");
      longjmp(env, CODE_TWO);
    case CODE_TWO:
      puts("Handling code two");
      break;
    default:
      puts("No matching handler");
      break;
  }

cleanup:
  puts("Cleanup path");
  return 0;
}
Starting work
Handling code two
Cleanup path

提示された実行例はステータス0で終了しました。この経路では、longjmpによってCODE_TWOを返す形でswitchが再開し、ハンドラーが実行された後、クリーンアップラベルへ進みます。このラベルは共通の処理経路を明示する別の方法です。上記のdo/whileの構成では、正常終了、対応する例外値による移行、対応しない値の返却のそれぞれについて、クリーンアップへ進む経路を明示する必要があります。longjmpは、確保したメモリを解放したり、ファイルを閉じたり、ロックを解除したり、デストラクターを実行したりしません。プログラムで対応する各経路に沿って、必要なリソースを明示的に解放してください。