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の落とし穴にはまるプログラマのムービー」にするべし、という助言を知人からもらいましたが、やめておきました。