スマートポインタ2
unique_ptr
スマートポインタのひとつ、auto_ptr
は非推奨であることはすでに説明しました。
(スマートポインタ1参照)
auto_ptr
を置き換えるものとして、std::unique_ptr
の使用が推奨されています。
auto_ptr
以外のスマートポインタの使用にはC++11以上に対応したコンパイラが必要です。
(Visual Studio 2015では対応済み)
//auto_ptr
std::auto_ptr<int> aPtr1(new int(1));
std::auto_ptr<int> aPtr2;
//所有権移動
aPtr1 = aPtr2;
//unique_ptr
std::unique_ptr<int> uPtr1(new int(1));
std::unique_ptr<int> uPtr2;
//コンパイルエラー
uPtr1 = uPtr2;
auto_ptr
ではインスタンスのコピーを行うと、実際にはコピーではなく所有権が移動します。
unique_ptr
はコピー操作自体が禁止されており、コンパイルエラーとなります。
そのためうっかり所有権が移動してしまうミスを防ぐことができます。
生ポインタの取得、所有権の取得、所有権の放棄などはauto_ptr
と同じです。
(get
関数、reset
関数、release
関数)
所有権の移動
unique_ptrで所有権を移動させるにはムーブという手法を用います。
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> ptr1(new int(1));
std::unique_ptr<int> ptr2;
//これはコンパイルエラー
//ptr2 = ptr1;
//ムーブ
ptr2 = std::move(ptr1);
//これ以降ptr1は使わないこと
//コンストラクタに直接指定してもよい
std::unique_ptr<int> ptr3(std::move(ptr2));
std::cout << *ptr3 << std::endl;
std::cin.get();
}
所有権はstd::move
関数を使用することで、他のunique_ptr
に移すことができます。
ムーブというのはC++11から追加された新しい概念で、スマートポインタ以外でも広く使われています。
ここでのムーブの目的は所有権の移動ですが、スマートポインタ以外ではコピー処理を減らして高速化するために使用されます。
とりあえずunique_ptr
の所有権の移動は上記のように行う、と覚えておいてください。
所有権を移動させた後は、元のインスタンスが管理するポインタはNULLになります。
ムーブについて詳しくはムーブセマンティクスを参照してください。
所有権の確認
unique_ptr
が実際にメモリを管理しているかどうかは、インスタンスを条件式に指定することで確認できます。
(get
関数の戻り値がNULLか否かで確認することも可能)
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> ptr1(new int(1));
std::unique_ptr<int> ptr2(std::move(ptr1));
if (ptr1)
std::cout << "所有権あり" << std::endl;
else
std::cout << "所有権なし" << std::endl;
if (ptr2)
std::cout << "所有権あり" << std::endl;
else
std::cout << "所有権なし" << std::endl;
std::cin.get();
}
所有権なし 所有権あり
unique_ptr同士の入れ替え
同じデータ型のunique_ptr
同士はswap
メンバ関数で互いの管理するデータを入れ替えることができます。
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> ptr1(new int(1));
std::unique_ptr<int> ptr2(new int(2));
//ptr1とptr2の中身の入れ替え
ptr1.swap(ptr2);
std::cout << *ptr1 << std::endl;
std::cout << *ptr2 << std::endl;
//2
//1
std::cin.get();
}
配列の管理
auto_ptr
では配列を扱えませんでしたが、unique_ptr
では可能です。
std::unique_ptr<int[]> ptr(new int[5]);
//auto_ptrだとエラー
//std::auto_ptr<int[]> ptr(new int[5]);
for (int i = 0; i < 5; i++)
ptr[i] = i;
データ型の指定に[]
を付けることで、配列を扱うことができます。
後は通常のポインタと同じ扱い方が可能です。
インスタンス破棄時には自動的にdelete[]
が実行されるので、メモリリークはありません。
deleter
スマートポインタのインスタンスが破棄される時に実行される関数をdeleter(デリータ)といいます。
デフォルトでは管理しているメモリに対してdelete
するだけの処理ですが、これを別のものに置き換えることができます。
(auto_ptr
ではデフォルトから置き換えることはできません)
例えばfopen
関数で取得したファイルポインタあればfclose
関数が必要ですし、malloc
関数で確保したメモリ領域ならばfree
関数の呼び出しが必要です。
これらのポインタを管理する場合は解放処理を適切に指定する必要があります。
deleterの指定方法はいくつかあります。
関数オブジェクト
deleterの指定は一般的には関数オブジェクトを使用します。
#include <iostream>
#include <memory>
//deleter
struct deleter_fclose
{
void operator()(FILE* fp) const
{
fclose(fp);
std::cout << "run deleter" << std::endl;
}
};
int main()
{
const char* file = "C:\\test.txt";
FILE* fpTmp;
if (fopen_s(&fpTmp, file, "r") != 0)
{
std::cout << file << "が開けません。" << std::endl;
std::cin.get();
return 0;
}
//普通に一文字取り出してみる
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //OK
{
//fpTmpをunique_ptrで管理する
//テンプレート第二引数にdeleter_fcloseを追加
std::unique_ptr<FILE, deleter_fclose> fp(fpTmp);
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //OK
std::cout << static_cast<char>(fgetc(fp.get())) << std::endl; //OK
}
//インスタンスfpの寿命はここまで
//つまりdeleterによりfpTmpにfcloseが実行されている
//すでにファイルはクローズされているので
//ファイルアクセスはNG
//std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl;
std::cin.get();
}
ここでは「C:\test.txt」の中身は「ABCDEFG」であるとします。
A B C run deleter
このコードで定義しているdeleter_fclose
は構造体ですが、C++の構造体はメンバに関数を持つことができます。
(C++でのstruct
はclass
とほぼ同じものです)
そして、関数呼び出し演算子()
を演算子オーバーロードしたものが関数オブジェクトです。
(演算子のオーバーロード)
()
演算子オーバーロードの引数にはunique_ptr
で管理する任意の型の引数を指定し、関数内ではそれを解放する処理を記述します。
今回はFILE*型の引数に対してfclose
関数を呼び出してます。
サンプルなので、deleterが呼び出されたことが分かるように「run deleter」という文字列も出力しています。
unique_ptr
の宣言ではテンプレート仮引数(データ型の指定)の第二引数に、先ほど定義した関数オブジェクトを指定します。
これでunique_ptr
のインスタンスが破棄される時に、deleterとして指定した関数オブジェクトが呼び出されるようになります。
fpTmp
はただのポインタ変数(ファイルポインタ)で、スマートポインタではないため、unique_ptr
のコンストラクタに渡した後もfpTmp
を使用することができてしまいます。
スマートポインタに管理を任せた後は使用しないように気を付けましょう。
unique_ptr
に渡した直後にfpTmp
にはNULL
を代入しておけば間違えて使用してしまう危険性は低くなります。
(ただしコンパイルは通るので、実行時エラーになる可能性はあります)
サンプルコードで使用しているfopen_s
関数の戻り値はエラーを示す値(error_t
型)で、unique_ptr
のコンストラクタに直接指定できません。
fopen
関数の場合はファイルポインタを返すので、unique_ptr
のコンストラクタに直接fopen
を記述でき、一時保存用の変数(サンプルではfpTmp
)は必要なくなります。
const char *file = "C:\\test.txt";
std::unique_ptr<FILE, deleter_fclose> fp(fopen(file, "r"));
Visual Studioでfopen
関数を使用するには設定を変更する必要があります。
(文字列のコピーのページ下段を参照)
関数を直接指定する
fclose
関数のように、終了処理用の関数があらかじめ存在する場合は関数を直接deleterに指定することもできます。
(自作関数でも良い)
#include <iostream>
#include <memory>
int main()
{
const char *file = "C:\\test.txt";
FILE *fpTmp;
if (fopen_s(&fpTmp, file, "r") != 0)
{
std::cout << file << "が開けません。" << std::endl;
std::cin.get();
return 0;
}
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //OK
{
//fclose関数を直接unique_ptrのdeleterとして指定する
std::unique_ptr<FILE, decltype(&fclose)> fp(fpTmp, fclose);
std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //OK
std::cout << static_cast<char>(fgetc(fp.get())) << std::endl; //OK
}//インスタンスfpの寿命はここまで
//NG
//std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl;
std::cin.get();
}
A B C
unique_ptr
のテンプレート引数の第二引数が変更されています。
ここにはdeleterとして指定したい関数の型情報を、decltype
を用いて指定します。
unique_ptr
のコンストラクタには引数がひとつ追加されています。
ここにはdeleterとして指定したい関数の関数名、今回はfclose
を指定します。
(丸括弧は付けない)
これでインスタンスfpが破棄される時に、fclose
関数が自動的に呼び出されるようになります。
decltypeは、指定した値のデータ型を取得するキーワードです。
例えばdecltype(1)
と記述した場合、数値リテラルはint型なのでそこに「int」と記述したのと同じ効果が得られます。
詳しくはdecltype指定子の項を参照してください。
//この二つは同じ意味
int a = 0;
decltype(0) b = 0;
//ただの文字列としての"int"ではなく
//コード上にintと記述したのと同じ意味になる
make_unique
unique_ptrのインスタンスの生成はstd::make_unique<データ型>(引数)
を使用することもできます。
std::unique_ptr<int> ptr1 = std::make_unique<int>(1);
//配列の場合(引数は要素数)
std::unique_ptr<int[]> ptr2 = std::make_unique<int[]>(5);
for (int i = 0; i < 5; i++)
ptr2[i] = i;
make_unique
はC++14以降に対応したコンパイラで使用できます。
(Visual Studio 2015は対応済み)