[2009 年 8 月号] |
[技術講座]
目次
1. スマートポインタとは?
2. サードパーティライブラリとの共存
2.1. クラスCacheがなぜshared_ptrを必要とするのか
2.2. なぜ、クラスCacheにshared_ptrを渡すことができないのか
3. weak_ptrの利用
4. まとめ
boost::weak_ptrを利用するケースについてお話しする前に、まず、スマートポインタについての概要をおはなしする必要があります。なぜなら、boost::weak_ptrは、(ちょっと特殊な)スマートポインタの一種だからです。
スマートポインタとは、賢い(スマートな)ポインタという名前の由来からも想像できるかと思いますが、指し示すオブジェクトの後始末を自動化してくれる、ポインタのように振る舞うクラステンプレートのことです。
プログラミング言語C++では、メモリや各種ハンドルなどのリソースの管理にスマートポインタを利用します。スマートポインタを用いることで、リソースの解放が自動化され、リソースリーク問題を防ぐことができます。
残念ながら、現在のC++(C++03)標準では、汎用的に利用できるスマートポインタが提供されていません(std::auto_ptrが提供されているのですが、利用における制約が多いため、利用可能なケースが限られます)。しかし、Boostライブラリを利用することで、様々なスマートポインタが利用可能になります。Boostライブラリに関しては、https://boost.cppll.jp/HEAD/にて日本語で紹介されているので参照してみてください。最新の情報は本家のサイト(英語)https://www.boost.org/を参照してください。
さて、今回筆者が利用したのは、boost::shared_ptrです。まず、boost::shared_ptrとてもシンプルな利用イメージを以下に示します。
※なお、記事のタイトルでもあるboost::weak_ptrは、boost::shared_ptrと合わせて使う、特殊なスマートポインタですが、これに関しては後ほど触れます。
#include <boost/shared_ptr.hpp> #include <iostream> class MyClass; typedef boost::shared_ptr<MyClass> MyClassSp; class MyClass { public: MyClass() { std::cout << "Created" << std::endl; } ~MyClass() { std::cout << "Destroyed" << std::endl; } }; int main() { std::cout << "Test started" << std::endl; { MyClassSp sp(new MyClass); } std::cout << "Test finished" << std::endl; }
このコードを実行すると、以下のように出力されるはずです。なお、このコードをコンパイルするためには、boostライブラリのパスをインクルードパスに指定する必要があります。
Test started Created Destroyed Test finished
boost::shared_ptrのインスタンスspが、スコープから外れたタイミングで指し示すオブジェクトをdeleteしていることがわかります。Test finishedが表示される前に、Destroyedが表示されていることで、確認できますね。
なお、boost::shared_ptrは参照カウント方式のスマートポインタで、shared_ptrオブジェクトをコピーコンストラクトする度に、参照カウントが増えていき、shared_ptrオブジェクトが破棄される度に参照カウントが減っていく仕組みとなっています。そして参照カウントがゼロになったタイミングで、指し示すオブジェクトをdeleteします。
Boostは、boost::shared_ptr以外にも様々なスマートポインタを提供しています。詳細は、以下を参照してください。
- 日本語 https://boost.cppll.jp/HEAD/libs/smart_ptr/smart_ptr.htm
- 本家(英語)https://www.boost.org/doc/libs/1_39_0/libs/smart_ptr/smart_ptr.htm
※ 日本語版は、最新版に基づく情報でない場合がありますので、ご注意下さい。
さて、このように便利なスマートポインタなのですが、サードパーティのライブラリとの共存においては工夫が必要な場合があります。まず、以下のクラス図をご覧ください。
ここで、青く色づけされたクラスと関連、汎化などの線に着目してください。これらがサードパーティのライブラリという想定です。クラスDownloaderは、操作downloadを呼び出すことで、引数で指定されたDownloadObjectに設定されたURIに存在するデータをhttpでダウンロードするという機能を提供します。また、DownloadObjectにはダウンロード要求の優先度も設定します。ライブラリ利用者は、DownloadObjectをサブクラス化して利用します。
当該オブジェクトのダウンロードが完了したら、downloadCompletedが呼び出されるので、これをオーバーライドして自前の処理を行います。これが、サードパーティのライブラリが提供する機能です。
一方、クライアント(サードパーティのライブラリを利用するアプリ)側は、リソースの管理をshared_ptr(以降本文中ではboost::を省略する)で行うポリシーとしています。クラスClientがContentを生成してshared_ptr<Content>として保持します。また、このアプリにはキャッシュメカニズムが存在します。キャッシュメカニズムは、クラスCacheがダウンロード済みのContentのshared_ptrを保持することで実現されます。こうしておくことで、Clientがダウンロード済みのContentを破棄しても、実際に破棄は行われず、キャッシュから再取得することが可能となります。なお、クラスCacheのインスタンスはシステムでひとつだけ存在し、グローバルに取得可能となっています(Singletonパターン)。
なお、ClientによるContentの破棄やCacheからのContentの取得といった細かい部分は、本記事の中心テーマではないため、省略しています。
さて、このような仕組みを実現するためには、サードパーティの提供するクラスDownloadObjectの操作downloadCompleted()をどのようにオーバーライドして実装すればよいでしょうか。以下のシーケンス図をご覧ください。
まず、Contentのライフラインに着目してください。ダウンロードが完了すると、4:downloadCompleted()が呼び出され、その内部で、4.1:render()を呼び出しています。ここでダウンロードしたコンテンツをレンダリングします。さらに、自前のキャッシュメカニズムにこのコンテンツを登録するために、4.3:addContent()を呼び出します。そのためには、Cacheオブジェクトにアクセスする必要がありますが、Singletonという前提なので、4.3:addContent()の直前で、4.2:getInstance()を呼び出して取得しています。
これでうまくいくはずなのですが、ひとつ問題があります。クラスCacheの操作addContent()が、パラメータとしてshared_ptr<Content>を要求しているのですが、これを渡すことができないという問題です。
ここで話を整理しましょう。ポイントは2つあります。ひとつは、「クラスCacheがなぜshared_ptr<Content>を必要とするのか」もうひとつは、「なぜ、クラスCacheにshared_ptr<Content>を渡すことができないのか」です。
2.1. クラスCacheがなぜshared_ptrを必要とするのか
まずは前者から考えてみましょう。クラスCacheがshared_ptr<Content>の代わりに、Content *を受けるというのはどうでしょうか?そうなっていれば、downloadCompleted()の内部でthisポインタを渡せばよいことになります。しかし、(スマートポインタではない)通常のポインタだと、Cache側で自動開放ができなくなってしまいます。では、addContent()の内部で引数で受けたContent *を元に、shared_ptr<Content>を作るというのはどうでしょうか?
これは、shared_ptrの参照カウントの仕組みが理解できていない、非常にまずいアプローチです。これではメモリを2重に解放してしまいます。以下のコードをご覧ください。CacheがContent *からshared_ptrを新たに作成すると、以下のコード同様のことが発生することになるのです。
#include <boost/shared_ptr.hpp> #include <iostream> class Content; typedef boost::shared_ptr<Content> ContentSp; class Content { public: Content() { std::cout << "Created" << std::endl; } ~Content() { std::cout << "Destroyed" << std::endl; } }; int main() { std::cout << "Test started" << std::endl; { Content *p = new Content; ContentSp sp1(p); // Clientにて保持 ContentSp sp2(p); // Cacheにて保持 } std::cout << "Test finished" << std::endl; }
sp2はsp1と同じオブジェクトを指していますが、sp1とsp2の間には何の関係もありません。よって、shared_ptrが同じオブジェクトを共有していることをsp1もsp2も知るすべがないのです。よって、それぞれshared_ptrがスコープから外れたときに、指し示すオブジェクトをdeleteします。その結果、同じオブジェクトが2回deleteされることになります。筆者の環境では、実行結果は、以下のような出力となりました。なお、メモリの2重解放が行われてしまっているため、環境によって実行した場合の振る舞いが異なります。
Test started Created Destroyed Destroyed Segmentation fault (core dumped)
shared_ptrは、他のshared_ptrからコピーコンストラクトするか、他のshared_ptrを代入された場合にのみ、その相手の指し示すオブジェクトを正しく共有することになります。よって、上記コードを以下のように修正すれば、Contentは一度だけ解放されるようになり、問題なく動作します。
#include <boost/shared_ptr.hpp> #include <iostream> class Content; typedef boost::shared_ptr<Content> ContentSp; class Content { public: Content() { std::cout << "Created" << std::endl; } ~Content() { std::cout << "Destroyed" << std::endl; } }; int main() { std::cout << "Test started" << std::endl; { Content *p = new Content; ContentSp sp1(p); // Clientにて保持 ContentSp sp2(sp1); // Cacheにて保持 } std::cout << "Test finished" << std::endl; }
つまり、コピーコンストラクトにせよ、代入にせよ、オブジェクトを共有するためには、共有元となるオブジェクトのポインタではなくshared_ptrが必要ということです。
2.2. なぜ、クラスCacheにshared_ptrを渡すことができないのか
さて次に、後者「なぜ、クラスCacheにshared_ptr<Content>を渡すことができないのか」について考えてみましょう。
問題は、クラスDownloaderの操作download()のパラメータとして、DownloadObject *を渡している点にあります。しかし、ここはサードパーティのライブラリであるため、修正することができません。そんなわけで、この問題は今のところ解決策がありません。ライブラリが提供する基底クラスをサブクラス化して利用するケースは、大抵これに当てはまります。
仮に、サードパーティのライブラリを修正できる状況あるいは、自分でライブラリを作成しているような状況で、クラスDownloaderの操作download()のパラメータをshared_ptr<DownloadObject>にすることができれば、問題は解決するのでしょうか?実は、それでもうまくいきません。詳細は、コラム「shared_ptrとポリモーフィズム」を参照してください。
なんとか、Contentのポインタから、shared_ptr<Content>を取り出す方法は無いものでしょうか?作り出すのが無理なら最初から埋め込んでみてはどうでしょう。つまり、クラスContentのメンバ変数としてshared_ptr<Content>を持たせるわけです。
class Content; typedef boost::shared_ptr<Content> ContentSp; class Content:public DownloadObject { public: static ContentSp create(std::string uri, int priority) { ContentSp s(new Content(uri, priority)); s->setSp(s); return s; } virtual ~Content() { std::cout << "Destroyed" << std::endl; } virtual void downloadCompleted() { std::cout << "downloadCompleted" << std::endl; Cache::getInstance().addContent(self_); } private: Content(std::string uri, int priority) :DownloadObject(uri, priority) { std::cout << "Created" << std::endl; } void setSp(const ContentSp &sp) { self_ = sp; } ContentSp self_; // 自身のshared_ptrを持つ。まずいアプローチ };
このアプローチによって、オーバーライドしたdownloadCompleted()の中から、自身のshared_ptrであるself_にアクセスすることが可能となりました。これをCacheに渡せばよいわけです。
なお、上記コードは、クラスContentの部分を抜粋しています。全体は、sp_self.cppとなります。さて、これを実行すると結果は以下のようになりました。
Test started Created downloadCompleted Test finished
クラスContentのデストラクタが呼ばれることで出力されるはずのDestroyedが出力されていません。これはなぜでしょう?
shared_ptrは、そのデストラクタが呼ばれるときに、参照カウントを1減算し、参照カウントが0になったときに、指し示すオブジェクトをdeleteします。しかし、class Contentのメンバ変数としてself_を持っており、これが自身を指し示しているため、Content::createを呼び出した直後の参照カウントは2となります。そして、利用側のshared_ptr<Content>がスコープから外れたときに、参照カウントが1になります。この段階で参照カウントが0になっていないため、オブジェクトがdeleteされません。参照カウントが0になるためには、Contentオブジェクトが破棄される必要があります。破棄するにはdelete呼び出しが必要であす。よって、いつまで経ってもオブジェクトが破棄されない、一種のデッドロックに陥っています。
こんな状況を解決するのが、weak_ptrです。weak_ptrは、参照カウントを増やさないスマートポインタの一種です。weak_ptrはshared_ptrと一緒に使うように設計されているため、単体で使うことはありません。
参照カウントを増やさないスマートポインタというと、通常のポインタと大差が無いように思いますが、最大の違いは、weak_ptrからはshared_ptrを取り出す(正しく共有する形で作り出す)ことができる点にあります。
class Content; typedef boost::shared_ptr<Content> ContentSp; typedef boost::weak_ptr<Content> ContentWp; class Content:public DownloadObject { public: static ContentSp create(std::string uri, int priority) { ContentSp s(new Content(uri, priority)); s->setWp(s); return s; } virtual ~Content() { std::cout << "Destroyed" << std::endl; } virtual void downloadCompleted() { std::cout << "downloadCompleted" << std::endl; Cache::getInstance().addContent(self_.lock()); } private: Content(std::string uri, int priority) :DownloadObject(uri, priority) { std::cout << "Created" << std::endl; } void setWp(const ContentSp &sp) { self_ = sp; } ContentWp self_; };
上のコード(完全なコードはwp_self.cpp)を見ると、class Contentのメンバ変数self_が、ContentWpになっています。ContentWpは、boost::weak_ptr<Content>のtypedefです。createメンバ関数が呼び出されると、privateメンバ関数setWpを通じてself_がセットされます。このように、weak_ptrは、shared_ptrとセットで利用します。そして、メンバ関数downloadCompleted定義の中で、class Cacheのメンバ関数addContentにself_を渡しているのですが、ここで、self_.lock()という呼び出しを行っています。この呼び出しによって、weak_ptrからshared_ptrを取り出しているのです。
なお、lock()は、クラステンプレートweak_ptrのメンバ関数です。このメンバ関数は、対応するshared_ptrが有効であるならば、すなわち参照カウントが1以上で、オブジェクトをまだdeleteしていないならば、そのオブジェクトのshared_ptrを返します。shared_ptrが無効になっているなら、shared_ptrをデフォルトコンストラクトして返します。shared_ptrをデフォルトコンストラクトすると、なにも指していない、NULLポインタのように振る舞うshared_ptrが作られます。
weak_ptrをメンバ変数として追加したクラス図を、以下に示します。
4. まとめ
今回は、スマートポインタの基本的な利用方法を紹介しました。特に、オーバーライドされたメンバ関数のthisポインタから、shared_ptrを取り出すために、weak_ptrが有効に利用できることを示しました。
このような構造は、今回例示したダウンローダだけでなく、GUIツールキットなどでもよく見かける構造です(Widgetをサブクラス化してイベントを受け取るケースなど)。weak_ptrは、様々な場面で、shared_ptrを用いて徹底的なリソース管理をしようとする際の、強力な支援手段となってくれることでしょう。
次回は、このような構造で表現されるクラスを保存・復元するためのシリアライズについて紹介する予定です。
© 2009 OGIS-RI Co., Ltd. |
|