スマートポインタ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++でのstructclassとほぼ同じものです)
そして、関数呼び出し演算子()を演算子オーバーロードしたものが関数オブジェクトです。
(演算子のオーバーロード)

()演算子オーバーロードの引数には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は対応済み)