C言語のプログラムフロー制御:setjmpとlongjmpによる非線形ジャンプ


仕組み

  • longjmp 関数は、setjmp で保存された jmp_buf 構造体を使用して、以前保存された呼び出しコンテキストに復元し、プログラムの実行をその場所へ移します。
  • setjmp マクロは、現在の呼び出しコンテキスト(スタックの状態、レジスタの値など)を jmp_buf 構造体に保存します。

使い方

#include <setjmp.h>

jmp_buf env;

int main() {
  int val = 0;

  if (setjmp(env) == 0) {
    val = 100;
    printf("val is %d before longjmp\n", val);
    longjmp(env, 1);
  } else {
    printf("val is %d after longjmp\n", val);
  }

  return 0;
}

この例では、setjmp が最初に呼び出されたとき、val は 100 に設定され、longjmp が呼び出される前にその値が出力されます。その後、longjmpenv を使用して setjmp の呼び出しコンテキストに復元し、プログラムの実行は setjmp の直後に移ります。2番目の setjmp の呼び出しでは、vallongjmp によって保存された値 (100) になるため、100 が出力されます。

用途

  • コルーチン:擬似的なコルーチンを実装することができます。
  • 長距離ジャンプ:深い関数呼び出し階層を飛び越えて、別の場所へジャンプすることができます。
  • エラー処理:エラーが発生したときに、エラーハンドラへジャンプすることができます。
  • C++ では、例外処理の方が setjmplongjmp よりも一般的に使用されています。
  • 複数の setjmplongjmp を組み合わせる場合は、呼び出しと復帰のペアを正しく管理する必要があります。
  • setjmplongjmp は、非ローカルジャンプを実現する強力な機能ですが、使い方を誤ると予期しない動作やプログラムクラッシュを引き起こす可能性があります。
  • 多くのライブラリやオペレーティングシステムは、setjmplongjmp と互換性のある機能を提供しています。
  • setjmplongjmp は、C言語の標準規格で定義されています。


エラー処理

この例では、平方根を求める関数 sqrt を作成し、setjmplongjmp を使用して、負の数の平方根を処理するエラーハンドラを実装します。

#include <setjmp.h>
#include <math.h>

jmp_buf env;

double sqrt(double x) {
  if (x < 0) {
    longjmp(env, 1);
  }
  return sqrt(x);
}

int main() {
  double x;
  int val;

  val = setjmp(env);
  if (val == 0) {
    x = -2.0;
    printf("sqrt(%f) = %f\n", x, sqrt(x));
  } else {
    printf("Error: cannot calculate square root of negative number\n");
  }

  return 0;
}

このコードを実行すると、以下の出力が得られます。

Error: cannot calculate square root of negative number

長距離ジャンプ

この例では、setjmplongjmp を使用して、長い関数呼び出し階層を飛び越えて、別の場所へジャンプします。

#include <setjmp.h>

jmp_buf env;

void level1() {
  printf("Level 1\n");
  level2();
}

void level2() {
  printf("Level 2\n");
  level3();
}

void level3() {
  if (setjmp(env) == 0) {
    printf("Level 3\n");
    longjmp(env, 1);
  } else {
    printf("Back to main()\n");
  }
}

int main() {
  level1();
  return 0;
}
Level 1
Level 2
Level 3
Back to main()

この例では、setjmplongjmp を使用して、擬似的なコルーチンを実装します。

#include <setjmp.h>

jmp_buf env[2];

void coroutine1() {
  printf("Coroutine 1: Start\n");
  longjmp(env[1], 1);
  printf("Coroutine 1: Resume\n");
}

void coroutine2() {
  printf("Coroutine 2: Start\n");
  longjmp(env[0], 1);
  printf("Coroutine 2: Resume\n");
}

int main() {
  int i = 0;

  while (1) {
    if (i == 0) {
      coroutine1();
      i = 1;
    } else {
      coroutine2();
      i = 0;
    }
  }

  return 0;
}
Coroutine 1: Start
Coroutine 2: Start
Coroutine 1: Resume
Coroutine 2: Resume
...

これらの例は、setjmplongjmp の基本的な使い方を示しています。これらの機能を組み合わせることで、より複雑なプログラムを実装することができます。

  • C++ では、例外処理の方が setjmplongjmp よりも一般的に使用されています。
  • setjmplongjmp は、非ローカルジャンプを実現する強力な機能ですが、使い方を誤ると予期しない動作やプログラムクラッシュを引き起こす可能性があります。
  • 上記の例はあくまでも説明を目的としたものであり、実際のプログラムで使用する場合には、適切なエラー処理や引数チェックなどを追加する必要があります。


  • C++: C++ では、例外処理の方が setjmplongjmp よりも一般的に使用されています。
  • 移植性: setjmplongjmp の実装はコンパイラによって異なる場合があり、移植性の問題を引き起こす可能性があります。
  • 複雑性: setjmplongjmp の使い方を誤ると、予期しない動作やプログラムクラッシュを引き起こす可能性があります。

これらの理由から、状況によっては setjmp の代替方法を検討することがあります。以下に、いくつかの代替方法を紹介します。

例外処理

C++ では、例外処理が setjmplongjmp の代替手段として一般的に使用されます。例外処理は、エラーが発生したときにプログラムの実行を別の場所へ移すためのより安全で構造化された方法を提供します。

#include <iostream>

void foo() {
  throw std::runtime_error("Error occurred");
}

int main() {
  try {
    foo();
  } catch (const std::exception& e) {
    std::cerr << "Caught exception: " << e.what() << std::endl;
  }

  return 0;
}

コールバック関数

コールバック関数は、別の関数を非同期的に呼び出すためのメカニズムです。setjmplongjmp と同様に、コールバック関数を使用して、プログラムの実行を別の場所へ移すことができます。

void callback() {
  printf("Callback function called\n");
}

int main() {
  void (*callback_ptr)() = callback;
  callback_ptr();

  return 0;
}

シグナル処理

シグナル処理は、プログラムがシグナルを受信したときに実行される関数を指定するためのメカニズムです。setjmplongjmp と同様に、シグナル処理を使用して、プログラムの実行を別の場所へ移すことができます。

#include <signal.h>

void signal_handler(int sig) {
  printf("Signal received: %d\n", sig);
  exit(1);
}

int main() {
  signal(SIGINT, signal_handler);

  while (1) {
    // ...
  }

  return 0;
}

GoTo 文

GoTo 文は、非ローカルジャンプを実現するための最も単純な方法ですが、構造化プログラミングの原則に反するため、一般的には推奨されていません。

void foo() {
  if (error) {
    goto error_handler;
  }

  // ...

error_handler:
  printf("Error occurred\n");
}

長距離ブランチ

長距離ブランチは、コンパイラによってサポートされている場合に使用できる、非ローカルジャンプを実現するための別の方法です。長距離ブランチは、setjmplongjmp よりも効率的で移植性が高い場合がありますが、すべてのコンパイラでサポートされているわけではありません。

これらの代替方法にはそれぞれ長所と短所があり、状況に応じて適切な方法を選択する必要があります。

  • 移植性:使用する代替方法がすべてのコンパイラでサポートされていることを確認する必要があります。
  • コードの複雑性:一部の代替方法は、setjmplongjmp よりも複雑なコードになる可能性があります。
  • 必要な機能:必要な機能によっては、特定の代替方法が適している場合があります。例えば、エラー処理が必要な場合は、例外処理が適切な選択肢となります。