スマートポインタを初期化する
まずは、初期化を見ていきましょう。unique_ptrやshared_ptrはテンプレート型のクラスとして実装されているので、そのインスタンスを生成して使うことになります。テンプレートについては、第2回で簡単に紹介しましたね。
スマートポインタの宣言[C++11]
スマートポインタは、unique_ptrなどのテンプレート型変数として宣言します。例えば、こんな感じです。
#include <memory> (1) using namespace std; (2) …略… // デフォルトコンストラクタで初期化する(nullptrが入る) unique_ptr<int> u_int_ptr; // int型のunique_ptr shared_ptr<string> s_str_ptr; // string型のshared_ptr
スマートポインタ(unique_ptrなどのクラス)の利用には、(1)のinclude文が必要です。また、クラスはstd名前空間に属するので、スコープ解決演算子(std::)を省略するために(2)のusing宣言を定義しています。
記述の通りunique_ptrもshared_ptrもクラスなので、型引数intおよびstringを伴ったインスタンスの生成であることは変わりません。このとき、デフォルトコンストラクタが呼ばれて安全に初期化されます。やっていることは、nullptr(0をdefineしたとかのNULLではなく、言語仕様でキッチリ決められたnull)を保持するということだけです。よって、生のポインタを初期値なしで宣言したときのような漏れはないのですが、nullptrであることには変わらないので、依然として不安な感じは残りますね。どうしましょうか。
初期値を与えてスマートポインタを宣言[C++11]
unique_ptr<T>やshared_ptr<T>のコンストラクタに引数を与えることによって、スマートポインタの初期値を指定することができます。
// コンストラクタに引数を与えて初期化する(初期値が入る)
unique_ptr<int> u_int_ptr2(new int(100)); // int型のunique_ptr
shared_ptr<string> s_str_ptr2(new string("Hello")); // string型のshared_ptr
コンストラクタの引数は、new演算子によって確保された動的なメモリ領域です。直値としての100、"Hello"などは指定できません。これは、スマートポインタはあくまでもメモリ管理のための構造なので、当たり前ですね。このときは、先の例とは違ってnullptrでない有効なメモリ領域のアドレスを保持します。100という整数が格納されているメモリ領域、そして"Hello"を値として持つstringが格納されているメモリ領域です。これで、上の例の不安はなくなります。よかったですね。
reset関数によるメモリ領域の割り当て[C++11]
最初の例のように、インスタンスを作成だけしておき、あとからメモリを割り当てることもできます。これは「所有権の委譲」といい、reset関数を使います。初期値のないnullptrのママのスマートポインタに、後からメモリを割り当てるというときに使います。
// あとからメモリ領域を割り当てる
u_int_ptr.reset(new int(200));
s_str_ptr.reset(new string("World"));
もし、u_int_ptrなどにすでにメモリ領域が割り当てられていたら、それは自動的に解放されます。所有されないメモリ領域が解放漏れで残ってしまう心配はないのです。安心ですね。
make_unique<T>/make_shared<T>関数による初期化[C++11][C++14]
ところで、ここまではメモリ領域の生成にnew演算子を使ってきました。おいおい、newは使わないで!じゃないのかい?やはりnewを使うというんじゃ、やっぱりdeleteも使うわ~なんてことにならないだろうね?という声があったかどうかは知りませんが、インスタンスの生成とメモリ領域の確保を一度にやってくれる便利な関数があります。それが、make_unique<T>関数とmake_shared<T>関数です。
// インスタンス生成とメモリ領域の割り当てを一度に行う
auto u_int_ptr3 = make_unique<int>(200); // C++14以降
auto s_str_ptr3 = make_shared<string>("Hello, World!!");
これは便利ですね。型推論が働くので宣言文がすごくスッキリします。型推論って何?という読者は第2回を読んでくださいね。これ、下手すると左辺にも右辺と同じ型名を書かなければなりません。autoって、なんて便利なんでしょう! なお、make_unique<T>関数はC++14以降でないとサポートされません。処理が複雑そうなmake_shared<T>関数の方じゃなくて?と思いますよね。 なお、nullptrのママのスマートポインタを生成したり、newを使うことがトラブルのもととなりうるので、特に理由がない限りmake_unique<T>関数とmake_shared<T>関数を使うことをおすすめしますね、アタシは。
[NOTE]make_shared関数を使うメリット
shared_ptrは、複数の所有権を管理できるという機能のために、値のためのメモリ領域の他に、参照カウンタのためのメモリ領域も確保、操作します。このため、何をするにもunique_ptr関数に比べると処理に時間がかかります。ですがmake_shared<T>関数を使うと、インスタンス初期化とメモリ領域の割り当てを一度に行ってくれるので、それぞれを別個に行うよりも速度上のメリットが発生します。なおさらこっちを使うべきですね。
make_unique<T>関数とmake_shared<T>関数は、Tのコンストラクタが複数の値をとる場合にも利用できます。例えば、タプルであるstd::tuple<T, ...>クラスのスマートポインタを利用したい場合、以下のようになります。
#include <tuple> // タプルの利用に必要 …略… // 複数の引数を持つコンストラクタを呼び出す auto u_tuple_ptr = make_unique<tuple<int, int, string>>(200, 10, "hello"); // C++14以降 auto s_tuple_ptr = make_shared<tuple<int, int, string>>(1000, 20, "world");
参考までに、タプルの要素へのアクセスは、std::get関数をget<0>(*u_tuple_ptr)のように呼び出します(この場合は0番目の要素)。u_tuple_ptrに「*」が付いてるけど、これってアレかな?という疑問には次節でお答えしますね。
[NOTE]make_unique_for_overwrite関数とmake_shared_for_overwrite関数
C++20以降では、make_unique_for_overwrite<T>関数とmake_shared_for_overwrite<T>関数が使えるようになりました。これらの関数は、make_unique<T>関数とmake_shared<T>関数のような値による初期化を行いません。つまり、あとで初期値を設定したりすることが前提の関数と言えます。これは、どうせ値を後で設定するのなら、わざわざ初期化するのはムダじゃね?ということもあってできた関数のようです。しかし、安全のために初期化しているのに、それをムダと言われるばかりか、値の初期化をプログラマに委ねるような昨今の状況に逆行するようなものなので、これらの利用は慎重になるべきでしょう。コンパイラ処理系によっては使用できないものもあるようです。
配列の利用[C++11][C++14][C++17][C++20]
スマートポインタを使うのはどのようなときでしょうか? そもそも、メモリを動的に確保したいというときは大きいデータを扱いたいときでしょうから、当然配列もその対象に入ってくるでしょう。スマートポインタで配列を使うのは簡単で、型パラメータをT[]のようにブラケット付きにするだけです。ただし、リスト中のコメントのように利用可能なバージョンが異なるので注意してください。もちろん、make_unique<T>関数などを使う方法が推奨されるのは同じです。
// 「型名[]」をテンプレート引数に指定してコンストラクタを呼ぶ unique_ptr<int[]> u_int_array_ptr(new int[10]); shared_ptr<string[]> s_str_array_ptr(new string[10]); //C++17以降 // make_unique<T>関数などを使う auto u_int_array_ptr2 = make_unique<int[]>(10); //C++14以降 auto s_str_arrau_ptr2 = make_shared<string[]>(10); //C++20以降
