悩ましきメモリ管理 - shared_ptr / weak_ptr
C/C++でのメモリ管理はプログラマに委ねられます。厄介なことにプログラマのちょっとしたミスが大事故に繋がるのが悩みのタネ。
string* p = new string("one"); string* q = new string("two"); p = q; // p,q 共に "two" を指す。 "one"は迷子に。 delete p; delete q; // "two"を多重delete!
std::auto_ptrの限界
標準C++ライブラリはメモリ管理を楽にするstd::auto_ptrを提供しています。
#include <iostream> #include <string> #include <memory> // std::auto_ptr<T> using namespace std; class item { private: string value_; public: item(const char* v="???") : value_(v) { cout << "item(" << value_ << ") ctor\n"; } ~item() { cout << "item(" << value_ << ") dtor\n"; } string value() const { return value_; } }; int main() { auto_ptr<item> p(new item("one")); auto_ptr<item> q(new item("two")); p = q; // pは指していた"one"をdeleteし、"two"を指す。qは空(null)となる。 cout << "p points to " << (p.get() ? p->value() : "(null)") << endl; cout << "q points to " << (q.get() ? q->value() : "(null)") << endl; }
item(one) ctor item(two) ctor item(one) dtor p points to two q points to (null) item(two) dtor
実行結果が示すとおり、コンストラクタとデストラクタの数が一致していますからdelete忘れや多重deleteが発生していないことが分かります。auto_ptrはデストラクト時にそれが指すポインタをdeleteするので明示的にdeleteする必要がありません。
ただし、auto_ptr間のコピー(代入)が行われるとポインタの所有権(=削除責任)が移動し、コピー元からはポインタが失われます(実行結果のqが"two"を指していませんね)。そのため複数のauto_ptrが一つのインスタンスを指して共有することができません。
class person { string name_; public: person(string n) : namae_(n) {} auto_ptr<person> child; // 子供 }; person sazae("サザエ"); person masuo("マスオ"); auto_ptr<person> tara(new person("タラ")); // "タラ"を"サザエ"と"マスオ"の子としたいが sazae.child = tara; // この瞬間"タラ"の所有権がtaraから // sazae.childに移動する(譲渡される)ため masuo.child = tara; // masuo.childは"タラ"を指せない
newされたインスタンスを自動的にdeleteしてくれるauto_ptrは便利には違いないのですが、この制限があるために用途が限られてしまうのです。
std::tr1::shared_ptr - 共有ポインタ
TR1で新たに追加されるshared_ptrは参照カウントというからくりでインスタンスを管理します。shared_ptrはその内部にそのインスタンスを参照しているshared_ptrの総数を保持しています。shared_ptrのデストラクタは内部の参照数を-1し、それが0となったときインスタンスがどこからも参照されなくなったと判断してインスタンスをdeleteします。
#include <iostream> #include <string> #include <boost/tr1/memory.hpp> // std::tr1::shared_ptr<T> using namespace std; class item { private: string value_; public: item(const char* v="???") : value_(v) { cout << "item(" << value_ << ") ctor\n"; } ~item() { cout << "item(" << value_ << ") dtor\n"; } string value() const { return value_; } }; int main() { // p1は"something"を指す。参照数:1 tr1::shared_ptr<item> p1(new item("something")); cout << "p1->value() = " << p1->value() << endl; { // p2も"something"を指す。参照数:2 tr1::shared_ptr<item> p2 = p1; cout << "p2->value() = " << p2->value() << endl; // ここでp2が消える。参照回数:1 } cout << "p1->value() = " << p1->value() << endl; // ここでp1が消える。参照数:0となり、"something"がdeleteされる }
item(something) ctor p1->value() = something p2->value() = something p1->value() = something item(something) dtor
std::tr1:weak_ptr 弱参照ポインタ
shared_ptrを使うことによって繁雑なメモリ管理から解放されるのですが、これでもまだ循環参照という問題が残っています。
#include <iostream> #include <string> #include <boost/tr1/memory.hpp> // std::tr1::shared_ptr<T> class Person { public: string name; // 名前 tr1::shared_ptr<Person> spouse; // 配偶者 Person(string n) : name(n) {} void info() const { cout << "My name is " << name << " and my spouse is " << spouse->name << endl; } }; int main() { // oneは"adam"を指す。 参照数:1 tr1::shared_ptr<Person> one(new Person("adam")); { // twoは"eve"を指す: 参照数:1 tr1::shared_ptr<Person> two(new Person("eve")); one->spouse = two; // "adam"の妻は"eve" 参照数:2 two->spouse = one; // "eve"の夫は"adam" 参照数:2 one->info(); two->info(); // ここでtwoが消える。参照数:1 ... 0ではないので"eve"はdeleteされない } one->info(); // ここoneが消える。参照数:1 ... 0ではないので"adam"はdeleteされない }
My name is adam and my spouse is eve My name is eve and my spouse is adam My name is adam and my spouse is eve
この例のように、複数のshared_ptrが互いを参照しあってループを形成する(循環する)と参照数が0になることがないため、インスタンスがdeleteされずに残ってしまうのです。
この問題を解消するため、TR1はさらにもう一つ、weak_ptrを提供します。weak_ptrはshared_ptrが保持する参照数の増減に関与しません。さらにweak_ptrのメンバexpired()によってインスタンスの消失を知ることができます。
#include <iostream> #include <string> #include <boost/tr1/memory.hpp> // std::tr1::shared_ptr<T>, std::tr1::weak_ptr<T> class Person { public: string name; tr1::weak_ptr<Person> spouse; Person(string n) : name(n) {} void info() const { cout << "My name is " << name << " and my spouse "; if ( spouse.expired() ) // インスタンスの有無を判断する cout << "has gone...\n"; else cout << spouse.lock()->name << endl; } }; int main() { // oneは"adam"を指す。 参照数:1 tr1::shared_ptr<Person2> one(new Person2("adam")); { // twoは"eve"を指す: 参照数:1 tr1::shared_ptr<Person2> two(new Person2("eve")); // weak_ptrは参照数の増減に関与しない one->spouse = two; // "adam"の妻は"eve" 参照数:1 two->spouse = one; // "eve"の夫は"adam" 参照数:1 one->info(); two->info(); // ここでtwoが消える。参照数:0となり"eve"はdeleteされる } one->info(); // ここoneが消える。参照数:0となり"adam"はdeleteされる }
My name is adam and my spouse eve My name is eve and my spouse adam My name is adam and my spouse has gone...
まとめ
次期C++規格「C++0x」で拡張予定のライブラリ「TR1」からarrayとshared_ptr/weak_ptrを紹介しました。これらを活用することでC++につきものの厄介なメモリ管理がぐっと楽になることでしょう。
TR1にはその他にも便利なクラスが多数収録されています。続編をお楽しみに。
