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