スマートポインタ2

unique_ptr

スマートポインタのひとつ、auto_ptrは非推奨であることはすでに説明しました。
(スマートポインタ1参照)
auto_ptrを置き換えるものとして、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」というバージョンのC++から追加された新しい概念で、スマートポインタ以外でも広く使われています。
これはなかなか難解で、ここでは説明できません。
とりあえず所有権の移動は上記のように行う、と覚えておいてください。

所有権を移動させた後は、元のインスタンスは使用しないようにしてください。
ただし、再度別のメモリ領域を割り当てて使用することはできます。

所有権の確認

インスタンスが実際にメモリを管理しているかどうかは、インスタンスを条件式に指定することで確認できます。
(get関数の戻り値をNULLまたは0と比較することでも可能)


#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();
}
所有権なし
所有権あり

配列

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 fclose_delete
{
    void operator()(FILE *fp) const
    {
        fclose(fp);
        std::cout << "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

    {
        std::unique_ptr<FILE, fclose_delete> fp(fpTmp);
        std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //OK
        std::cout << static_cast<char>(fgetc(fp.get())) << std::endl; //OK
    }//インスタンスfpの寿命はここまで

    std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //NG

    std::cin.get();

「C:\test.txt」の中身は以下にしています。


ABCDEFG
A
B
C
deleter
・

「fclose_delete」は構造体ですが、C++での構造体はメンバに関数を持つことができます。
(C++でのstructはclassとほぼ同じものです)
そして、関数呼び出し演算子「operator()」をオーバーロードしたものが関数オブジェクトです。
(演算子のオーバーロード)

オーバーロードの処理ではfclose関数を呼び出しているだけです。
サンプルなので、deleterが呼び出されたことが分かるように「deleter」の文字列も出力しています。

unique_ptrの宣言ではテンプレート引数(データ型の指定)の第二引数に、先ほど定義した関数オブジェクトを指定します。
これでunique_ptrのインスタンスが破棄される時にdeleterが呼び出されるようになります。

fpTmpはただのポインタ変数(ファイルポインタ)で、スマートポインタではないため、unique_ptrのコンストラクタに渡した後もfpTmpを使用することができてしまいます。
スマートポインタに管理を任せた後は使用しないように気を付けましょう。
unique_ptrに渡した直後にfpTmpにはNULLを代入しておけば間違えて使用してしまう危険性は低くなります。
(ただしコンパイルは通ってしまいます)
もちろん、unique_ptrのインスタンス破棄するとfclose関数が呼ばれますから、fpTmpは使用できなくなります。

fopen_s関数の戻り値はエラーを示す値(error_t型)なので、unique_ptrのコンストラクタに直接記述することはできません。
fopen関数の場合はファイルポインタを返すので、unique_ptrのコンストラクタに直接fopenを記述でき、一時保存用の変数(サンプルではfpTmp)は必要なくなります。


const char *file = "C:\\test.txt";
std::unique_ptr<FILE, fclose_delete> 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

    {
        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の寿命はここまで

    std::cout << static_cast<char>(fgetc(fpTmp)) << std::endl; //NG

    std::cin.get();
}
A
B
C
・

unique_ptrのテンプレート引数の第二引数が変更されています。
ここにはdeleteとして指定したい関数の型情報を、decltypeを用いて記述します。

そして、コンストラクタに引数がひとつ追加されています。
ここにはdeleterとして指定したい関数の関数名を記述します。
(丸括弧は付けない)

これでインスタンスfpが破棄される時に、fclose関数が自動的に呼び出されるようになります。

decltypeは、指定した式のデータ型を取得するキーワードです。


int funcA()
{
    std::cout << "funcA呼び出し" << std::endl;
    return 0;
}

int funcB()
{
    std::cout << "funcB呼び出し" << std::endl;
    return 0;
}


int main()
{
    int a = 10;
    decltype(a) b = 20; //変数bはint型
    decltype(funcA()) c = 30;  //変数cはint型。関数の戻り値の型を使用

    std::cout << typeid(b).name() << std::endl;
    std::cout << typeid(c).name() << std::endl;

    decltype(&funcA) d = funcA; //関数funcAのポインタを取得
    d(); //関数funcAの呼び出し

    d = funcB; //変数dを関数funcBの別名に変更
    d(); //関数funcBの呼び出し

    std::cin.get();
}
int
int
funcA呼び出し
funcB呼び出し

decltypeは引数に指定された式のデータ型を取得し、そのデータ型がそこに記述されているのと同じ意味を持たせることができます。
特定の変数と同じ型の変数を宣言できるほか、関数ポインタを取得することもできます。

関数ポインタは、数値などの値に対するポインタと考え方は同じです。
変数のメモリ上の位置(アドレス)が分かればどこからでもアクセスが可能なのと同じで、関数のメモリ上の位置が分かればどこからでも呼び出しが可能となります。

関数を実行する場合、普通はコード中に関数名を記述して関数を呼び出しますが、この方法で呼び出せる関数は固定です。
上記コードのように、実行したい関数を動的に指定・変更したい場合に関数ポインタは使用されます。
サンプルコードの24行目と27行目では、同じ「d();」という呼び出し方で、違う関数を呼び出せていることがわかります。
ただし、指定できる関数は戻り値の型と引数の種類(データ型と数)が同じ関数同士に限ります。

関数ポインタを取得するには、関数名の前にアドレス取得演算子(&)を付けてdecltypeに渡します。
その際に丸括弧(関数呼び出し演算子)は付けません。

ちなみに、20行目と21行目のtypeidは指定した式のデータ型の情報(std::type_info&型)を取得する演算子です。
上記コードのように、取得した値のメンバ関数nameを使用すると、データ型名を取得することができます。
これはただの文字列ですので、decltypeのようにデータ型を記述する代わりとして使用することはできません。

typeidを使用するにはRTTIを有効にする必要があるかもしれません。
(新しいキャストのページ下部を参照)

関数ポインタの話はちょっと難しいかもしれないので、とりあえずunique_ptrのdeleterに関数を指定する場合はこのサンプルコードのようにする、と覚えておくだけでかまいません。

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は対応済み)