[C言語] setjmp() と longjmp() の使いかた

C言語の標準ライブラリ関数 setjmp() と longjmp() を呼び出すことで多段の関数呼出階層を飛び越えるジャンプ(いわゆるGOTO処理)を実現できます。しかしながら、現代的なプログラミングでは GOTO文 が忌避されるように、setjmp() と longjmp() を使ったジャンプも推奨されません。やむを得ず setjmp() と longjmp() で実装された既存のソースコードを理解するための助けとなることを目論んだ解説です。

1. 単純なロングジャンプの例

1.1. サンプル・ソースコード

1.2. サンプル・ソースコードの実行結果

  1. 関数 main() の中から top_function() を top_function() の中から middle_function() を middle_function() の中から bottom_function() を順に呼び出しています。
  2. 関数 main() の中で top_function() を呼び出していますが、次行の printf() を実行していません。
  3. 関数 top_function() の中で middle_function() を呼び出していますが、次行の printf() を実行していません。
  4. 関数 middle_function() の中で button_function() を呼び出していますが、次行の printf() を実行していません。
  5. 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出していますが、次行の printf() を実行していません。
  6. 関数 buttom_function() の中で標準ライブラリ関数 longjmp() を呼び出した直後に main() 中のelse句を実行しています。

1.3. シーケンス図

1.3.1. もし return文 で関数呼出しを1段ずつ戻ったとき

return文のシーケンス

1.3.2. longjmp() で関数呼出先(末端)から一足飛びに戻ったとき

longjmpのシーケンス

1.4. 解説

関数 main() の中で setjmp() を呼び出したタイミングで、longjmp() からジャンプ(復帰)してきたときに復元するスタック情報を静的な変数 jmp_buf jump_buffer に保存しています。もし、longjmp() で復帰してくる前に jump_buffer の内容を壊してしまうと、ジャンプで戻ってきたは良いものの関数 main() は続きの処理を正しく実行できません。もし longjmp() で戻ってこなければ jump_buffer に保存した情報は不要になります。

関数 setjmp() を最初に呼び出したときは 必ず 0 (ゼロ) を返します。次に longjmp() からジャンプ(復帰)してきたときは必ず非ゼロを返すため、else句に分岐します。

関数 top_function() だけに着目すると middle_function() からreturn文で戻ってきて次行の printf() を実行することを期待します。しかし longjmp() で一足飛びに関数 main() に戻って(ジャンプして)しまうためソースコードから動作を追跡することが難しくなります。

同様に middle_function() だけに着目すると buttom_function() からreturn文で戻ってきて次行の printf() を実行することを期待します。しかし longjmp() で一足飛びに関数 main() に戻って(ジャンプして)しまうためソースコードから動作を追跡することが難しくなります。

上記の例では関数 bottom_function() の中で必ず longjmp() を呼び出しているため、常に button_function() から top_function() までのreturn文が省略(スキップ)されています。しかし longjmp() を呼び出す条件を分岐すれば、ジャンプ処理/return文が実行されたり、されなかったりとなります。

2. 繰り返し処理の中でロングジャンプをつかった例

2.1. サンプル・ソースコード

2.2. サンプル・ソースコードの実行結果

  1. 関数 start_engine() の中で乱数を生成し、longjmp()を実行したりしなかたり分岐しています。
  2. 関数 start_engine() の中でロングジャンプを実行しないときは、関数 countdown() の中の処理(含むforループ)を全て実行します。
  3. 関数 start_engine() の中でロングジャンプを実行したときは、関数 countdown() の中の続きの処理が丸っとスキップして、 main() に戻ります。

2.3. 解説

いわゆるエラーが発生したときに、以降の処理を丸っとスキップする流れが簡易に実現できます。この例では関数呼び出しが2階層と浅く、longjmp() を呼び出す箇所が一箇所であるため、ソースコードから全体の処理の流れを追跡するいことは比較的容易です。しかし、関数呼出しが4階層、5階層と深くなり、longjmp() を使ってジャンプする箇所が複数存在すると、メンテナンスやデバッグが難しいプログラムになります。

3. ロングジャンプの弊害

  1. プログラムに必要な後始末(たとえばOpen処理に対するClose処理)が漏れる(意図せずにスキップする)ミスを誘発します。
  2. ソースコードの『ここ』を通過するはず、とprintfデバッグを埋め込んだり、デバッガでブレイクポイントを仕掛けても、するっと通り抜けてしまいます。

4. 参考リンク