オープンソースのライブラリのソースコードを呼んでいるとC++11で追加されたunique_ptrを使用しているところもちらほらと見られるようになってきています。
MFCなどを利用していれば動的確保したメモリ以外にもHINSTANCE型やHBITMAP型など後処理が必要なリソースを扱うことが多いはずです。
Effective C++などでも解説されているように「リソースの確保が初期化」(RAII)をしっかりと守るためにスマートポインタは早いうちから使い方に慣れていくことが大事になります。
今回はLoadLibrary関数から取得したHINSTANCE(HMODULE)型をunique_ptrとして管理してみましょう。
unique_ptr?
名前からも推測できるように、リソースの所有権を一意(unique)にもっているように振る舞うスマートポインタ(賢いポインタ)です。
スマートポインタとは?
任意のリソースを指し示すポインタです。
スマートポインタが削除される際(スコープから抜けるなど)にそのスマートポインタが指すリソースも正しく削除されるため、リソースを管理する場合はこういったスマートポインタを利用するべきです。
同様に振る舞うスマートポインタ、auto_ptrを置き換えるものとして追加されました。
所有権を一意に持つ?
auto_ptrはコピーをする際に、コピー元をNULLにし同じリソースを指すスマートポインタを複数持つような動作はできない仕組みになっていました。
1 2 3 4 5 6 7 8 9 10 |
#include <memory> // auto_ptr, unique_ptr int main() { std::auto_ptr<int> p1(new int(10)); std::auto_ptr<int> p2(p1); if (!p1.get()) std::cout << "コピーした際に所有権がp2に移っているため、p1はNULLを指す。" << std::endl; } |
同じリソースを指すスマートポインタが複数あれば、複数回deleteが実行され未定義の動作が実行されてしまうため、この方法でそういった実装ができないようになっていました。
unique_ptrも同様に同じリソースを指すスマートポインタを複数持つことはできないように実装されています。
1 2 3 4 5 6 7 8 9 10 11 |
#include <memory> int main() { std::unique_ptr<int> p1(new int(10)); // std::unique_ptr<int> p2(p1); std::unique_ptr<int> p2(std::move(p1)); if (!p1) // operator boolも定義されています! std::cout << "コピーした際に所有権がp2に移っているため、p1はNULLを指す。" << std::endl; } |
ただし、4行目のような初期化はできなくなっています。
所有権を移動させるにはstd::move関数(5行目)を使用します。
解放処理
auto_ptrでは解放の処理は必ずdeleteが実行されるため、動的確保した配列には使用することはできませんでした。
1 2 3 4 5 6 7 8 9 10 |
#include <memory> int main() { // 配列はdelete[]で解放しなければいけないのに、auto_ptrはdeleteを呼んでしまう。 // そしてコンパイルエラーにならない! // std::auto_ptr<int> p1(new int[10]); return 0; } |
unique_ptrは配列に対しても正しくdelete[]が呼ばれるようにテンプレートとして特殊化されているので安心して使用することができます。
1 2 3 4 5 6 7 8 |
#include <memory> int main() { std::unique_ptr<int> p1(new int[10]); // OK! return 0; } |
リンク:std::unique_ptr | cpprefarence.com
また専用のデリータ関数(リソースを解放するための関数)を自分で定義することもできるようになっています。
これはサンプルプログラムを見てもらった方がいいかもしれません。
HINSTANCE型のリソースを解放する
紹介するサンプルプログラムではDLLファイルを読み取り、そのまま解放するだけのプログラムです。
DLLファイルの読み取りはLoadLibrary関数を呼び、後処理にはFreeLibrary関数を呼ばなければいけません。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <Windows.h> #include <memory> struct Library_deleter { typedef HMODULE pointer; inline void operator()(HMODULE module) const { FreeLibrary(module); } }; int main() { std::unique_ptr<HMODULE, Library_deleter> hDLL(LoadLibrary(L"ignore.dll")); return 0; } |
実はHMODULE型やHINSTANCE型は単なるvoid *型ではなく、HINSTANCE__構造体のポインタとして定義されています。
またHMODULE型は、
288 |
typedef HINSTANCE HMODULE |
として定義されているので、HINSTANCE型とHMODULE型は全く同じ型です。
HINSTANCEについてはリンク先のページに詳しいです。
リンク:HINSTANCE ‐ 通信用語の基礎知識
また、解放にはdeleteやdelete[]ではなく、FreeLibrary関数を呼ぶ必要があるためデリータを定義する必要があります。
さて、2行目の「typedef HMODULE pointer;」はとても不思議に見えます。
デリータが指定された場合、unipue_ptrの実装ではpointer型(テンプレート引数のtypedef)の存在をチェックします。
pointer型が存在している場合は、デリータの引数にpointer型を指定し、デリータを呼び出すようになっており、存在しない場合は第一引数の型を使用するようになっています。
実際にプログラムを打ち込んで、2行目をコメントアウトすると代入できないといったエラーメッセージが表示されるはずです。
リンク:c++ – Using std::unique_ptr for Windows HANDLEs – Stack Overflow
実行コストは?
さて、リソース管理に大変便利なunique_ptrですが、実行コストはどれくらいなのでしょうか。
実は生のポインタを使用する場合とほぼ違いがありません。
そのため、特に明確な理由がなければリソース管理でunique_ptrを使わない手はないのです。
試しに、上記のサンプルプログラムをunique_ptrを使用した場合と、直接LoadLibrary/FreeLibraryを使用した場合でそれぞれ10,000回呼び出した際の実行時間を確認したところ、以下のような結果でした。
unique_ptr<HMODULE, Library_deleter> | |
最速 | 19200ms |
最低 | 21414ms |
平均 | 20404.8ms |
LoadLibrary / FreeLibrary(raw) | |
最速 | 20163ms |
最低 | 21442ms |
平均 | 20698.7ms |
最適化の影響なのか、unique_ptrを使った方が早いくらいでした。
この結果から実行時コストを気にしてunique_ptrを使わないといった判断はできないことが分かります。
まとめ
少し記事が長くなってしまいましたが、C++11で実装されたunique_ptrの有用性を伝えることができたでしょうか?
オープンソースなどでは新しい技術なども使われたりするのでとても勉強になります。(意味が分からずに困惑することもありますが…。)
これからリソースの管理を行う場合にはunique_ptrをまず考えてみてください。
多くの場合はunique_ptrを使うことが正解になると思いますヨ。