見出し画像

続C言語教室 - 第23回 setjmp と longjmp

その昔、GOTOという構文が嫌われた話は以下の記事に書いたのですが、プログラミング言語に充分な機能がない場合、それを補うためにGOTO(もしくは類似の機能)を使わざる得ないことはママあることです。

GOTO論争の記憶

良く必要に迫られるのが複数のループを一度に抜ける大域脱出と何らかの異常が発生したために通常処理から抜け出て特別な処理がしたい例外処理があります。多くのモダンな処理系では例外処理機能を持つようになりましたが(Go言語には無いんですよね)、C言語には相変わらずありません。

だいたいに於いて例外処理とは古のMS-DOSでのCOMMAND.COMでのエラーのように「中止、再試行、失敗」のいずれかになるように処理を落とし込むことでもあります。

Abort, Retry, Fail?

https://ja.wikipedia.org/wiki/Abort,_Retry,_Fail%3F

ですから例外処理でやることは、例外の原因となった処理をやり直すか、そのままか何らかの処理が継続できるデータなりを補って継続するか、すべての処理をやめてプログラムを終えてしまうかのいずれかにはなります。もちろん処理をするために使ったメモリやファイルなど、特にプロセスを超えて使ったもののお片付けも忘れてはいけません。

そんな例外処理を setjmp を使うことで書こうと思えば書くことも出来ます。まず基本的な使い方としては、setjmp を実行して、例外が発生した時に戻るべき場所を用意します。この時にコンテクストを保存するための変数(jmp_buf)が必要になるのですが、他の関数からもアクセスできる必要があるので、基本的にはグローバル変数に置きます。setjmp は実行したときには 0 が戻り、longjmp から飛んできたときには 0 以外が戻ります。そこで 0 の時がそのまま処理が進んだ場合で、言ってみればここからが try ブロックになります。0 以外が戻った場合が catch ブロックに相当し、ここで例外に対処するコードを書くわけです。そして throw に相当するのが longjmp になるわけです。

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

jmp_buf jbuf; // jmp buffer

void func(void) {
  printf("func()\n");
  longjmp(jbuf, 1);
}

int main() {
  int jval;
  printf("start\n");
  if ((jval = setjmp(jbuf)) == 0) {
    // こちらは普通の処理
    func();
  } else {
    // longjmp から来た時
    switch(jval) {
    case 1:
      printf("longjmp from 'func'\n");
      break;
    }
    return 1;
  }
  printf("end\n");
  return 0;
}

start
func()
longjmp from 'func'

実行結果

例外処理は、使ったことがあればご存知の通り、一般的に入れ子にして使いたいことが普通です。まず main で全体に対する最初の setjmp を実行しておきます。longjmp はどこにあるかわかりませんので、ここで間違いなく有効な jmp_buf を与えるのを忘れてはいけません。それ以降、特定の状況での例外を捕まえたいときには、外側の jmp_buf を保存しローカルな jmp_buf を設定します。これで新たな内側の try ブロックが作れるわけです。そして catch にあたる部分で、元の jmp_buf を復元し、継続するか、ここで longjmp で元の例外処理に任せるわけです。

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

jmp_buf jbuf; // jmp buffer

void func1(void) {
  printf("func1\n");
  longjmp(jbuf, 1);
}

void func2(void) {
  int jval;
  jmp_buf bjbuf;

  // 元からあるバッファを退避
  memcpy(&bjbuf, &jbuf, sizeof(jmp_buf));

  printf("func2\n");
  if ((jval = setjmp(jbuf)) == 0) {
    // 普通の処理
    func1();
  } else {
    // この関数で処理する内容
    printf("caught longjmp\n");
    // バッファを復元
    memcpy(&jbuf, &bjbuf, sizeof(jmp_buf));
    // このまま元の例外に戻す場合
    longjmp(jbuf, 2);
  }
 }

int main() {
  int jval;
  printf("start\n");
  if ((jval = setjmp(jbuf)) == 0) {
    // こちらは普通の処理
    func2();
  } else {
    // longjmp から来た時
    switch(jval) {
    case 1:
      printf("longjmp from 'func1'\n");
      break; 
    case 2:
      printf("longjmp from 'func2'\n");
      break;
    }
    return 1;
  }
  printf("end\n");
  return 0;
}

start
func2
func1
caught longjmp
longjmp from 'func2'

実行結果

このように、C++の例外処理とほぼ同じように書くことができるのです。C++でも同じなのですが、setjmp を最初に実行した時点から変化したグローバルなリソースを巻き戻す必要があるので、何を戻さなければならないのかを正確に設計して記述しておく必要があります。これがなかなか骨ではあるのですが、一番大切なところです。おかげで状態を記録して戻す処理が、本来の処理より長くなることすらあったりするんですよね。setjmp の例外処理の部分では、setjmp 実行後のローカル変数の値がどうなっているかは慎重に見極める必要があったりするので、テストコードを書いて確認しておくことを薦めます(static は安心なんですが、今度は入れ子になった場合に困ることがあります)。

データベースで言えばトランザクションとして結果を巻き戻す処理をキチンと書けということでもあるのですが、異常が起こった際に処理をヤメてしまえば良いと済ませるのではなく、ちゃんと後片付けを最初から考えておいてねというのは、他のプログラムに迷惑を書けない良いコードになります。一時ファイルやロックファイルを残したままだったり、共有メモリを掴んだままにされちゃうと迷惑千万なんですよ。というわけで後片付けはちゃんとしましょうという話でした。

ヘッダ画像は、以下のものを使わせて頂きました。https://commons.wikimedia.org/wiki/File:Abort_Retry_Fail.PNG
By Benjamin Scott (Wikipedia user DragonHawk) - Screen shot of MS-DOS 7.x (from Win98SE), running in a virtual machine., Public Domain, https://commons.wikimedia.org/w/index.php?curid=39765561

#C言語 #プログラミング #プログラミング講座 #C言語教室 #setjmp #longjmp #例外処理 #MSDOS #後片付け #巻き戻し


いいなと思ったら応援しよう!

kzn
頂いたチップは記事を書くための資料を揃えるために使わせていただきます!