今回は「スマートポインタ (Smart Pointer)」を取り上げます。以前のC++には、標準ライブラリの <memory> に auto_ptr<T> というスマートポインタが用意されてました。最近の規格 (C++11) では、auto_ptr<T> の使用は推奨されておらず、新しいスマートポインタ unique_ptr<T>, shared_ptr<T>, weak_ptr<T> が導入されました。今回はこの中から unique_ptr<T> の基本的な使い方について簡単に説明します。
C++の場合、動的なメモリの取得と解放は new と delete で行います。ポインタをそのまま扱う場合、delete を忘れたときにメモリリークする危険性があり、メモリ管理はけっこう面倒なものになります。スマートポインタは名前が表しているように、ポインタをスマートに扱うためのクラステンプレートです。
スマートポインタはポインタによって初期化され、ポインタと同じように扱うことができます。また、スマートポインタにはデストラクタがあるので、デストラクタを実行するとき、ポインタが指し示しているメモリを解放することができます。
auto_ptr<T> はポインタ T* を保持し、それを「所有権の移動」で管理しています。コピーコンストラクタや代入演算子の処理が実行されると、そのポインタの所有権はコピー先 (または代入先) に移り、元の auto_ptr が保持しているポインタ変数は 0 に書き換えられます。
auto_ptr は C++11 よりも前の規格で作られたクラステンプレートなので、ムーブコンストラクタやムーブ代入演算子はありません。所有権を移動したことを忘れて、うっかり元の auto_ptr にアクセスするとコアダンプが発生します。次の例を見てください。
リスト : auto_ptr の使用例 (sample2106.cpp) #include <iostream> #include <memory> using namespace std; class Foo { int x; public: Foo(int n) : x(n) { cout << "constructor\n"; } ~Foo() { cout << "destructor\n"; } int get() const { return x; } }; int main() { auto_ptr<Foo> a(new Foo(1)); cout << a->get() << endl; auto_ptr<Foo> b = a; // 所有権の移動 cout << b->get() << endl; // cout << a->get() << endl; コアダンプ auto_ptr<Foo> c; c = b; // 所有権の移動 cout << c->get() << endl; // cout << b->get() << endl; コアダンプ }
$ clang++ sample2106.cpp ・・・ warning (省略) ・・・ $ ./a.out constructor 1 1 1 destructor
コンパイルするとワーニングが表示されますが、プログラムは実行することができます。auto_ptr のコンストラクタにはポインタを渡します。これで auto_ptr はそのポインタが示すメモリ領域を保持し、自分に所有権があればデストラクタでメモリ領域を解放します。
変数 a を変数 b の初期値として指定すると、コピーコンストラクタが呼び出されますが、このとき auto_ptr はメモリの所有権を移動します。a->get() を実行するとコアダンプが発生します。同様に、c = b は代入演算子の処理が呼び出され、ここでも auto_ptr の所有権は移動します。このあと、b->get() を実行してもコアダンプが発生します。main() が終了すると、デストラクタが実行されます。
このように、auto_ptr はうっかり所有権を移動すると、そのあとでコアダンプを発生させる危険性があります。また、コピー元や代入元のインスタンスを破壊的に修正するため、auto_ptr を標準的なコンテナクラスに格納することは推奨されていません。
C++11 から導入された unique_ptr は、ただ一つの unique_ptr だけがメモリの所有権を所持しているスマートポインタです。具体的には、auto_ptr と同様に所有権の移動で所持したポインタを管理します。auto_ptr と違って、unique_ptr を他の unique_ptr にコピーしたり代入することはできません。そのかわり、ムーブコンストラクタとムーブ代入演算子が用意されているので、move() を使って明示的に所有権を移動することができます。
簡単な使用例を示しましょう。
リスト : unique_ptr の使用例 (sample2107.cpp) #include <iostream> #include <memory> using namespace std; class Foo { int x; public: Foo(int n) : x(n) { cout << "constructor\n"; } ~Foo() { cout << "destructor\n"; } int get() const { return x; } }; int main() { unique_ptr<Foo> a(new Foo(1)); cout << a->get() << endl; // unique_ptr<Foo> b = a; コンパイルエラー unique_ptr<Foo> b = move(a); // 明示的に所有権を移動 cout << b->get() << endl; unique_ptr<Foo> c; // c = b; コンパイルエラー c = move(b); // 明示的に所有権を移動 cout << c->get() << endl; }
$ clang++ sample2107.cpp $ ./a.out constructor 1 1 1 destructor
unique_ptr を使うときはヘッダファイル memory をインクルードしてください。所有権は move() で移動することができます。コピーコンストラクタや代入演算子を呼び出す処理はコンパイルエラーになります。
auto_ptr は配列を所持することはできませんが、unique_ptr ならば可能です。次のように、テンプレート仮引数に T [] を指定してください。
unique_ptr<T []> a(new T [size]);
あとはコンストラクタの引数に、動的に生成した配列を渡すだけです。この場合、添字演算子 [] を使って配列の要素にアクセスすることができます。簡単な例を示しましょう。
リスト : 配列を所持する (sample2108.cpp) #include <iostream> #include <memory> using namespace std; int main() { unique_ptr<int []> a(new int [10]); for (int i = 0; i < 10; i++) a[i] = i; for (int i = 0; i < 10; i++) cout << a[i] << endl; }
$ clang++ sample2108.cpp $ ./a.out 0 1 2 3 4 5 6 7 8 9
もちろん、インスタンスを格納する配列も unique_ptr に格納することができます。次の例を見てください。
リスト : 配列を所持する (sample2109.cpp) #include <iostream> #include <memory> using namespace std; class Foo { int x; public: Foo() : x(0) { cout << "constructor\n"; } Foo(int n) : x(n) { cout << "constructor " << n << endl; } ~Foo() { cout << "destructor " << x << endl; } int get() const { return x; } void put(int n) { x = n; } }; int main() { unique_ptr<Foo []> a(new Foo [8]); for (int i = 0; i < 8; i++) a[i].put(i); for (int i = 0; i < 8; i++) cout << a[i].get() << " "; cout << endl; }
$ clang++ sample2109.cpp $ ./a.out constructor constructor constructor constructor constructor constructor constructor constructor 0 1 2 3 4 5 6 7 destructor 7 destructor 6 destructor 5 destructor 4 destructor 3 destructor 2 destructor 1 destructor 0
このように、unique_ptr が廃棄されたとき、各々のインスタンスのデストラクタが実行されます。
unique_ptr は auto_ptr と違ってコンテナクラスに格納することができます。たとえば、vector に格納する場合は次のようになります。
リスト : vector に unique_ptr を格納する (sample2110.cpp) #include <iostream> #include <memory> #include <vector> using namespace std; class Foo { // // ・・・ 省略 ・・・ // }; int main() { vector<unique_ptr<Foo>> a; a.emplace_back(new Foo(1)); a.emplace_back(new Foo(2)); a.emplace_back(new Foo(3)); cout << a[0]->get() << endl; cout << a[1]->get() << endl; cout << a[2]->get() << endl; a.pop_back(); a.pop_back(); a.pop_back(); }
$ clang++ sample2110.cpp $ ./a.out constructor 1 constructor 2 constructor 3 1 2 3 destructor 3 destructor 2 destructor 1
Foo のインスタンスを保持する unique_ptr を vector に追加する場合、push_back よりも emplace_back を使ったほうが簡単です。pop_back で vector から unique_ptr を取り出すと、unique_ptr が廃棄されて、所持しているインスタンスのデストラクタが実行されます。
今まで所持しているメモリの所有権を放棄して、他のメモリの所有権を持ちたい場合はメンバ関数 reset を使います。reset は所持しているメモリを解放したあと、引数に渡されたメモリの所有権を所持します。引数なして reset を呼び出す、または引数に nullptr を渡すと、所持しているメモリを解放することができます。
なお、所有権の放棄はメンバ関数 release で行うことができます。release は所持していたポインタを返します。ポインタが指し示すメモリ領域は解放しないので注意してください。
unique_ptr が所持しているポインタはメンバ関数 get で求めることができます。get を呼び出したあとでも、unique_ptr は所有権を放棄しません。それから、unique_ptr には operator bool が多重定義されていて、条件式の中で unique_ptr を判定すると、所有権を持っていれば true を、そうでなければ false を返します。
簡単な使用例を示します。
リスト : メンバ関数の使用例 (sample2111.cpp) #include <iostream> #include <memory> using namespace std; class Foo { // // ・・・ 省略 ・・・ // }; int main() { unique_ptr<Foo> a(new Foo(1)); cout << a->get() << endl; cout << a.get() << endl; a.reset(new Foo(2)); cout << a->get() << endl; cout << a.get() << endl; if (a) cout << "true\n"; else cout << "false\n"; Foo* b = a.release(); if (a) cout << "true\n"; else cout << "false\n"; delete b; }
$ clang++ sample2111.cpp $ ./a.out constructor 1 1 0x5597cf2aeeb0 constructor 2 destructor 1 2 0x5597cf2af2e0 true false destructor 2
reset を実行すると、所持していた Foo のインスタンスがデストラクタで解放されます。変数 a が格納しているポインタの値も変わっています。if の条件式で変数 a を判定すると true になります。次に、release で所有権を放棄します。返り値は変数 b にセットします。if の条件式で変数 a を判定すると false になります。今まで所持していた Foo のインスタンスは解放していないので、delete b で解放します。