スマートポインタ3

shared_ptr

unique_ptr(やauto_ptr)は、管理するメモリ領域の所有権はただ一つのインスタンスのみが持つことが許されています。
(スマートポインタ1スマートポインタ2参照)
shared_ptrは、複数のインスタンスが所有権を持てるスマートポインタです。


#include <iostream>
#include <memory>

class TestClass
{
    int num;
public:
    TestClass(int n) : num(n)
    {}

    ~TestClass()
    {
        std::cout << "デストラクタ: " << num << std::endl;
    }

    void Add(int n)
    {
        num += n;
    }

    void Print()
    {
        std::cout << num << std::endl;
    }
};

int main()
{
    {
        std::shared_ptr<TestClass> ptr1 = std::make_shared<TestClass>(10);

		//↓でも可
		//std::shared_ptr<TestClass> ptr1(new TestClass(10));

        {
            std::shared_ptr<TestClass> ptr2(ptr1);  //所有権のコピー
            {
                std::shared_ptr<TestClass> ptr3;
                ptr3 = ptr1;  //所有権のコピー

                ptr3->Add(1);
                ptr3->Print();

            }//ptr3の寿命はここまで
             //TestClassのデストラクタは呼ばれない

            ptr2->Add(2);
            ptr2->Print();

        }//ptr2の寿命はここまで
         //TestClassのデストラクタは呼ばれない

        ptr1->Add(3);
        ptr1->Print();

    }//ptr1の寿命はここまで
     //ここでTestClassのデストラクタが呼ばれ、メモリが解放される

    std::cin.get();
}
11
13
16
デストラクタ: 16

shared_ptrで新しくメモリ領域を確保する場合、std::make_shared<データ型>(引数)でインスタンスを生成することが推奨されます。
shared_ptrは内部的な処理の都合上、今までunique_ptrなどで行っていた、newでインスタンスを生成してコンストラクタに渡す方法よりも処理効率が良いためです。

shared_ptrのインスタンスに、別のshared_ptrのインスタンスのコピーを行うと、所有権をコピーすることができます。
「=」による代入でも、コンストラクタ(コピーコンストラクタ)に指定しても同じです。

shared_ptrは、所有権を持つインスタンスが増えるたびに内部的にカウンタを増やし、インスタンスが破棄されるたびにカウンタを減らします。
そして、所有権を持つインスタンスがゼロになった時点でdeleteが呼び出され、メモリが解放されます。

生ポインタの取得、所有権の取得はauto_ptr、unique_ptrと同じです。
(get関数reset関数)
(スマートポインタ1参照)
ただし、所有権を放棄するrelease関数はshared_ptrには存在しません。

所有権の移動

shared_ptrは所有権のコピー、所有権の移動の両方が可能です。
所有権の移動はムーブで行います。


#include <iostream>
#include <memory>

class TestClass
{
    //上のサンプルコードと同じなので省略
};

int main()
{
    {
        std::shared_ptr<TestClass> ptr1 = std::make_shared<TestClass>(1);
        std::shared_ptr<TestClass> ptr2(ptr1); //所有権のコピー

        {
            //所有権の移動(ムーブ)
            std::shared_ptr<TestClass> ptr3(std::move(ptr1));
            //これ以降ptr1は使えない

            ptr3->Print();
        }//ptr3の寿命はここまで

        ptr2->Print();

        std::unique_ptr<TestClass> u_ptr1(new TestClass(2));

        //unique_ptrから所有権を移動
        ptr2 = std::move(u_ptr1);
        //最初のメモリ領域に対する所有権がすべて消滅
        //デストラクタが呼ばれる

    }//ptr2の寿命はここまで
    //デストラクタが呼ばれる

    std::cin.get();
}
1
1
デストラクタ: 1
デストラクタ: 2

所有権の移動は、同じデータ型のshared_ptrはもちろん、同じデータ型のunique_ptrからも可能です。
当然ながら元のunique_ptrのインスタンスは所有権を失います。

所有権を持つインスタンス数の確認

メモリの所有権を持つインスタンスの数はuse_countメンバ関数で取得できます。
また、メモリの所有権を持つのは自分だけか否かはuniqueメンバ関数で判定できます。


#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr1 = std::make_shared<TestClass>(1);
    std::cout << ptr1.use_count() << std::endl;

    if(ptr1.unique())
        std::cout << "所有権は自身のみ" << std::endl;
    else
        std::cout << "複数が所有権を所持" << std::endl;

    {
        std::shared_ptr<int> ptr2(ptr1);
        std::cout << ptr1.use_count() << std::endl;

        if (ptr1.unique())
            std::cout << "所有権は自身のみ" << std::endl;
        else
            std::cout << "複数が所有権を所持" << std::endl;
    }

    std::cout << ptr1.use_count() << std::endl;

    if (ptr1.unique())
        std::cout << "所有権は自身のみ" << std::endl;
    else
        std::cout << "複数が所有権を所持" << std::endl;

    std::cin.get();
}
1
所有権は自身のみ
2
複数が所有権を所持
1
所有権は自身のみ

配列

shared_ptrは配列を扱う機能がありません。
しかし少し工夫をすれば配列を扱うことができます。

ただし、配列を使う場合はstd::make_sharedでインスタンスを生成できません。
make_sharedはdeleterを指定できないためです。


#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> ptr(new int[5], std::default_delete<int[]>());

    for (int i = 0; i < 5; i++) 
        ptr.get()[i] = i;

    std::cin.get();
}

deleterの指定

テンプレート引数には配列(角括弧付き)ではなく、通常のデータ型(角括弧なし)を指定します。
そして、コンストラクタの第一引数はnew演算子で配列を確保し、第二引数には「std::default_delete<データ型[]>()」を指定します。
これはdeleterの指定です。

shared_ptrは配列に対応していないため、テンプレート引数に配列を指定できませんし、デフォルトのdeleterは「delete」が実行されるだけです。
newで確保した配列は「delete[]」で破棄しなければならないので、このままでは正常にメモリを解放できません。
そのため、deleterを配列用に置き換えます。

デフォルトのdeleterは「default_delete」という名前で用意されています。
このdefault_deleteのテンプレート引数を配列で指定することで、deleterは「delete[]」を実行するようになります。

各要素へのアクセス

各要素へのアクセスはget関数で先頭要素のアドレスを取得した後に添字演算子でアクセスします。
やや奇妙に見えるかもしれませんが、配列名に角括弧を付けて各要素にアクセスするのとやっていることは同じです。
(配列名は、配列の先頭要素へのポインタを返す)

weak_ptr

weak_ptrはshared_ptrとセットで使用されるスマートポインタです。
weak_ptrはshared_ptrが管理するメモリ領域にアクセスが可能ですが、所有権を持ちません。
(弱参照という)

shared_ptrはその性質上、循環参照という問題が起きます。


#include <iostream>
#include <memory>

class TestClass
{
public:
    std::shared_ptr<TestClass> ptr;

    TestClass(){ std::cout << "コンストラクタ" << std::endl; }
    ~TestClass() { std::cout << "デストラクタ" << std::endl; }
};


int main()
{
    {
        std::shared_ptr<TestClass> ptr1 = std::make_shared<TestClass>();
        std::shared_ptr<TestClass> ptr2 = std::make_shared<TestClass>();

        ptr1->ptr = ptr2;
        ptr2->ptr = ptr1;
    }
    //メモリは解放されない

    std::cin.get();
}
コンストラクタ
コンストラクタ

このコードのように、shared_ptrのインスタンス同士でお互いを所有した状態にします。
すると、shared_ptrのデストラクタでdeleteが実行されることがなく、メモリ領域はいつまで経っても解放されません。

これはつまり、

  • ptr1の寿命が尽きる
    • ptr2がptr1を所有しているので、deleteされない
  • ptr2の寿命が尽きる
    • 先ほどの処理でptr1は削除されていない
    • ptr1がptr2を所有しているので、deleteされない

という流れになります。
試しに、20行目もしくは21行目を削除した状態で実行してみてください。
正常にデストラクタが呼ばれるはずです。

この問題はweak_ptrで解決できます。


#include <iostream>
#include <memory>

class TestClass
{
public:
    std::weak_ptr<TestClass> ptr;

    TestClass(){ std::cout << "コンストラクタ" << std::endl; }
    ~TestClass() { std::cout << "デストラクタ" << std::endl; }
};


int main()
{
    {
        std::shared_ptr<TestClass> ptr1 = std::make_shared<TestClass>();
        std::shared_ptr<TestClass> ptr2 = std::make_shared<TestClass>();

        ptr1->ptr = ptr2;
        ptr2->ptr = ptr1;
    }
    //メモリ解放

    std::cin.get();
}
コンストラクタ
コンストラクタ
デストラクタ
デストラクタ

TestClassのメンバ変数をshared_ptrからweak_ptrに変更しただけです。
weak_ptrはshared_ptrを参照できますが所有権を持つことはありません。
そのため、インスタンスが互いに所有し合っている状態は起こらず、正常にメモリが解放されます。

参照の取得

shared_ptrが管理するメモリ領域を参照するにはコンストラクタに指定するほか、コピー操作で行うこともできます。
他のweak_ptrから参照を得るにはコピー操作、あるいはムーブで可能です。
ムーブにより参照を取得した場合、元のweak_ptrは参照を失います。


std::shared_ptr<int> sPtr = std::make_shared<int>(10);

std::weak_ptr<int> wPtr1(sPtr);  //sPtrを参照
std::weak_ptr<int> wPtr2 = sPtr; //sPtrを参照

std::weak_ptr<int> wPtr3(wPtr1);  //wPtr1を参照(つまりsPtrを参照)
std::weak_ptr<int> wPtr4 = wPtr1; //wPtr1を参照(つまりsPtrを参照)

std::weak_ptr<int> wPtr5(std::move(wPtr1));  //wPtr1の参照を奪う
//これ以降wPtr1は使えない

参照先の値へのアクセス

weak_ptrは他のスマートポインタとは違い、インスタンスをそのまま生ポインタのように扱うことはできません。
(間接演算子(*記号)やアロー演算子(->)は使えない)
参照先の値にアクセスするにはlock関数を使用します。
lock関数は参照先のshared_ptrを返しますので、これを介して値にアクセスします。
weak_ptrはメモリ領域の所有権を持たないので、使用中に参照先が解放されてしまうのを防ぐためです。


    std::shared_ptr<int> sPtr = std::make_shared<int>(10);
    std::weak_ptr<int> wPtr(sPtr);

    {
        std::shared_ptr<int> sPtrTmp = wPtr.lock();
        std::cout << *sPtrTmp << std::endl;
    }

    //インスタンスを生成しない場合
    std::cout << *(wPtr.lock()) << std::endl;

参照の解放

参照の解放はreset関数で行います。
reset関数は参照を解放するだけで、他のweak_ptrの参照を得るなどの機能はありません。


std::shared_ptr<int> sPtr = std::make_shared<int>(10);
std::weak_ptr<int> wPtr(sPtr);

wPtr.reset(); //参照を解放

参照先の状態確認

参照先のshared_ptrが解放されているか否かはexpired関数で確認できます。
参照先のshared_ptrのメモリ領域の所有者数はuse_count関数で確認できます。


#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sPtr1 = std::make_shared<int>(10);
    std::shared_ptr<int> sPtr2(sPtr1);
    std::weak_ptr<int> wPtr(sPtr1);

    std::cout << wPtr.use_count() << std::endl;

    if(wPtr.expired())
        std::cout << "解放済み" << std::endl;
    else
        std::cout << "解放されていない" << std::endl;

    sPtr1.reset();
    sPtr2.reset();

    if (wPtr.expired())
        std::cout << "解放済み" << std::endl;
    else
        std::cout << "解放されていない" << std::endl;

    std::cin.get();
}
2
解放されていない
解放済み