2007年1月 6日

RAII と setjmp/longjmp

C++ には RAII (Resource Acquisition Is Initialization) というイディオムがあります。これはリソースの開放を確実に行うためのテクニックとして C++ のプログラムで広く用いられています。しかし、RAII も setjmp/longjmp という落とし穴があります。

 

RAIIの基本

次のプログラムはメモリリークを起こします。

#include <string>
using namespace std;
int main() {
  string *p = new string;
  return 0;
}

このプログラムをビルドして valgrind でテストすると次のようなエラーメッセージが表示されます。4バイトのメモリリークが検出されました。

% g++ test.cc
% valgrind ./a.out
(省略)
==16657== LEAK SUMMARY:
==16657==    definitely lost: 4 bytes in 1 blocks.
==16657==      possibly lost: 0 bytes in 0 blocks.
==16657==    still reachable: 0 bytes in 0 blocks.
==16657==         suppressed: 0 bytes in 0 blocks.
==16657== Use --leak-check=full to see details of leaked memory.

メモリリークを防ぐには p を delete する必要があります。

#include <string>
using namespace std;
int main() {
  string *p = new string;
  delete p;  // delete する
  return 0;
}

この程度のプログラムなら delete を忘れることはなさそうですが、 new したオブジェクトが増えたり、関数の出口が増えたりすると、delete を確実に行うのは難しくなります。次の例を見てみましょう。

#include <string>
using namespace std;
int main(int argc, char **argv) {
  string *p = new string;
  if (argc == 1) {
    delete p;
    return 1;
  }
  string *q = new string;
  if (argc == 2) {
    delete p;
    delete q;  // ここでは q も
    return 1;
  }
  delete p;
  // q の開放を忘れている!
  return 0;
}

このプログラムをビルドして、argc が 3となるよう ./a.out foo bar のように実行すると、 q が指すオブジェクトのメモリはリークします。

そこで、 C++ ではRAII の出番です。RAII はローカル変数のデストラクタを使ってリソースの開放を行います。C++の標準ライブラリには auto_ptr という RAII 用のクラスがあるので、ここではこれを使います。Boost を使っている場合は scoped_ptr を使うといいでしょう。

#include <memory>
#include <string>
using namespace std;
int main(int argc, char **argv) {
  auto_ptr<string> p(new string);
  if (argc == 1) {
    return 1;
  }
  auto_ptr<string> q(new string);
  if (argc == 2) {
    return 1;
  }
  return 0;
}

上のコードでは new された 2つのオブジェクトは、p と q のスコープが閉じるときに、それぞれのデストラクタによって開放されます。このため、argc の値に関わらず、メモリリークは発生しません。

RAII と setjmp/longjmp

前置きが長くなりましたが、このように非常に便利な RAII も、 setjmp/longjmp 関数と同時に用いられると、落とし穴にはまることがあります。さっそく例を見てみましょう。

#include <setjmp.h>
#include <memory>
#include <string>
using namespace std;
void foo(jmp_buf env) {
  longjmp(env, 1);  // A地点に飛ぶ
}

int main(int argc, char **argv) {
  jmp_buf env;
  if (setjmp(env) != 0) {
    // A
    return 1;
  }
  auto_ptr<string> p(new string);
  foo(env);
  return 0;
}

上のプログラムをビルドして実行すると、auto_ptr の p が管理するオブジェクトはメモリリークを起こします。

コンパイラは関数の出口が複数あるコードや goto を含むコード、例外を含むコードでもスコープの最後でローカル変数のデストラクタが確実に呼ばれるようにコードを生成しますが、setmp/longjmp を使って大域ジャンプするようなコードには対処できません。

ローカル変数のデストラクタが呼ばれない問題は RAII に限った話ではないので、auto_ptr<string> p(new string); の代わりに、次のようなオブジェクトを作ってもリークが発生します。

  ...
  if (setjmp(env) != 0) {
    // A
    return 1;
  }
  string s(1000, 'a');  // 1000文字分の 'a' の文字列
  foo(env);
  ...

解決策としては、上の auto_ptr の例の場合、p の宣言を setjmp() の前に移動する方法があります。

#include <setjmp.h>
#include <memory>
#include <string>
using namespace std;
void foo(jmp_buf env) {
  longjmp(env, 1);  // A 地点に飛ぶ
}

int main(int argc, char **argv) {
  auto_ptr<string> p(new string);  // ここに移動
  jmp_buf env;
  if (setjmp(env) != 0) {
    // A
    return 1;
  }
  foo(env);
  return 0;
}

上のコードでは A 地点の return のところで p のデストラクタが呼ばれるため、メモリリークは発生しません。

まとめ

本記事では C++ の RAII の基本と、 setjmp/longjmp による落とし穴を紹介しました。Cで実装されたライブラリの中にはエラー処理を setjmp/longjmp を用いて行うもの (たとえば libpng はクライアント側で setjmp してから一連のライブラリ関数を呼びます) もあるため、注意が必要です。「setjmp() の後では RAII は安全ではない」というのが今回のポイントでした。

余談

タイトルを「物凄い勢いで RAII と setjmp/longjmpの落とし穴にはまるプログラマのムービー」にするべし、という助言を知人からもらいましたが、やめておきました。