スマートポインタ3

shared_ptr

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


#include <iostream>
#include <memory>
#include <string>

class TestClass
{
    const std::string name;
public:
    TestClass(const char* s) : name(s)
    {}

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

    void print(const char* s) const
    {
        std::cout << "name: " << name << ", ";
        std::cout << "呼び出し: " << s << std::endl;
    }
};

int main()
{
    {
        std::shared_ptr<TestClass> ptr1;
        {
            std::shared_ptr<TestClass> ptr2;
            {
				//shared_ptrの作成
                std::shared_ptr<TestClass> ptr3
					= std::make_shared<TestClass>("instance1");

                //↓でも可
                //std::shared_ptr<TestClass> ptr3(new TestClass("instance1"));

                //所有権のコピー
                ptr2 = ptr3;
                ptr1 = ptr2;

                ptr3->print("ptr3");
            }
            //ptr3の寿命はここまで
            //TestClassのデストラクタは呼ばれない
            std::cout << "(ptr3 has expired)" << std::endl;

            ptr2->print("ptr2");
        }
        //ptr2の寿命はここまで
        //TestClassのデストラクタは呼ばれない
        std::cout << "(ptr2 has expired)" << std::endl;

        ptr1->print("ptr1");
    }
    //ptr1の寿命はここまで
    //ここでTestClassのデストラクタが呼ばれ、メモリが解放される
    std::cout << "(ptr1 has expired)" << std::endl;

    std::cin.get();
}
name: instance1, 呼び出し: ptr3
(ptr3 has expired)
name: instance1, 呼び出し: ptr2
(ptr2 has expired)
name: instance1, 呼び出し: ptr1
デストラクタ: instance1
(ptr1 has expired)

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

shared_ptrのインスタンスに対するコピー操作は所有権のコピーとなります。
=による代入操作でも、コピーコンストラクタに指定しても同じです。

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

unique_ptrと共通する関数

生ポインタの取得はget関数、所有権の取得方法はreset関数で行います。
互いが管理するデータの入れ替えはswap関数が使用できます。

所有権を放棄するrelease関数はshared_ptrには存在しません。
reset関数に引数を指定せずに実行することで所有権を放棄できます。

所有権の移動

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


#include <iostream>
#include <memory>

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

int main()
{
    {
        //shared_ptrの作成
        std::shared_ptr<TestClass> ptr1
			= std::make_shared<TestClass>("instance1");
        {
            //所有権のコピー
            std::shared_ptr<TestClass> ptr2(ptr1);
            {
                //所有権の移動(ムーブ)
                std::shared_ptr<TestClass> ptr3(std::move(ptr1));
                //これ以降ptr1は空

                ptr3->print("ptr3");
				//ptr1->print("ptr1"); //エラー
            }//ptr3の寿命はここまで
            std::cout << "(ptr3 has expired)" << std::endl;

            ptr2->print("ptr2");

            //新しいunique_ptrの作成
            std::unique_ptr<TestClass> u_ptr(new TestClass("instance2"));

            //unique_ptrから所有権を移動
            ptr2 = std::move(u_ptr);
            //これ以降u_ptrは空

            ptr2->print("ptr2");

        }//ptr2の寿命はここまで
        std::cout << "(ptr2 has expired)" << std::endl;
    }//ptr1の寿命はここまで
    std::cout << "(ptr1 has expired)" << std::endl;

    std::cin.get();
}
name: instance1, 呼び出し: ptr3
(ptr3 has expired)
name: instance1, 呼び出し: ptr2
デストラクタ: instance1
name: instance2, 呼び出し: ptr2
デストラクタ: instance2
(ptr2 has expired)
(ptr1 has expired)

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

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

メモリの所有権を持つインスタンスの数はuse_countメンバ関数で取得できます。
また、メモリの所有権を持つのは自分だけか否かはuniqueメンバ関数で判定できます。
uniqueメンバ関数はC++17で非推奨となり、C++20で削除されました。
同等の処理はuse_count関数の戻り値が1か否かを調べることで可能です。
ただし、0を返す場合はデータを管理していません。


#include <iostream>
#include <memory>

void showOwnership(std::shared_ptr<int>& p)
{
    std::cout << "use_count: " << p.use_count() << std::endl;
    //if (p.unique()) //←C++20からは使用不可
    switch (p.use_count())
    {
    case 0:
        std::cout << "管理データなし" << std::endl;
        break;
    case 1:
        std::cout << "所有権は自身のみ" << std::endl;
        break;
    default:
        std::cout << "複数が所有権を所持" << std::endl;
    }
}

int main()
{
    std::shared_ptr<int> ptr1 = std::make_shared<int>(1);
    showOwnership(ptr1);

    {
        std::shared_ptr<int> ptr2(ptr1);
        showOwnership(ptr1);
    }
    //ptr2の寿命はここまで

    showOwnership(ptr1);

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

配列の管理

shared_ptrC++17から配列型に対応しています。
また、std::make_shared関数はC++20から配列型に対応しています。


#include <iostream>
#include <memory>

int main()
{
    //C++20
    std::shared_ptr<int[]> ptr1 = std::make_shared<int[]>(5);

    //C++17
    std::shared_ptr<int[]> ptr2 = std::shared_ptr<int[]>(new int[5]);

    for (int i = 0; i < 5; i++)
        ptr1[i] = i;
    for (int i = 0; i < 5; i++)
        ptr2[i] = i;

    for (int i = 0; i < 5; i++)
        std::cout << ptr1[i];
    std::cout << std::endl;
    for (int i = 0; i < 5; i++)
        std::cout << ptr2[i];

    std::cin.get();
}
01234
01234

C++14まで

C++14までのshared_ptrは素では配列には対応しておらず、少し工夫をする必要があります。


#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();
}

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

デフォルトのdeleterはstd::default_deleteという名前で用意されています。
このdefault_deleteのテンプレート引数に配列型を指定することで、オブジェクトの破棄時にdelete[]を実行するようになります。

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

weak_ptr

weak_ptrshared_ptrとセットで使用されるスマートポインタです。
weak_ptrshared_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;
    }
	//この時点でptr1とptr2にアクセス不可能になるが
	//メモリは解放されていない

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

コンストラクタの実行は確認できますが、デストラクタは一度も呼ばれていません。
shared_ptrのインスタンス同士でお互いを所有した状態にすると、メモリがいつまで経っても解放されなくなります。
これはつまり、

  1. まずptr1の寿命が尽きる
    • ptr2ptr1を所有しているので、deleteされない
  2. 次にptr2の寿命が尽きる
    • ptr1ptr2を所有しているので、deleteされない

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

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


#include <iostream>
#include <memory>

class TestClass
{
public:
    //weak_ptrに変更
    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_ptrshared_ptrを参照できますが所有権を持つことはありません。
そのため、インスタンスが互いに所有しあっている状態は起こらず、正常にメモリが解放されます。

weak_ptrが弱参照を持っている間に参照元のshared_ptrの数がゼロになった場合はその参照先のデータは消滅するので注意してください。

参照の取得

shared_ptrが管理するメモリ領域を参照するにはコンストラクタに指定するほか、コピー操作で行うこともできます。
他のweak_ptrから参照を得るにはコピー操作、あるいはムーブ(std::move())で可能です。
ムーブにより参照を取得した場合、元の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は管理データの所有権を持たないので、使用中に参照先が解放されてしまうのを防ぐためにこのような処理が必要になります。

lock関数の実行時に参照先が解放されていた場合、空の(何も管理していない)shared_ptrが返されます。


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

{
	//以下のコメントアウトを解除すると結果が変わる
	//sPtr.reset();

	std::shared_ptr<int> sPtrTmp = wPtr.lock();

	if(sPtrTmp)
		std::cout << *sPtrTmp << std::endl;
	else
		std::cout << "参照切れ" << std::endl;
}
10

参照の解放

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


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

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

参照の入れ替え

互いが管理するデータの入れ替えはswapメンバ関数で行うことができます。


#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> sPtr1 = std::make_shared<int>(1);
    std::shared_ptr<int> sPtr2 = std::make_shared<int>(2);

    std::weak_ptr<int> wPtr1 = sPtr1;
    std::weak_ptr<int> wPtr2 = sPtr2;

    wPtr1.swap(wPtr2);

    std::cout << *wPtr1.lock() << std::endl;
    std::cout << *wPtr2.lock() << std::endl;
    //2
    //1

    std::cin.get();
}

参照先の状態確認

参照先の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
解放されていない
解放済み

なお、マルチスレッドプログラムにおいて、参照先をlock関数で取得できるか否かのチェックする目的でexpired関数を使用することはできません。
マルチスレッドでは、たとえコード上では連続した処理であっても、expired関数の実行とlock関数の実行にはわずかな時間差があり、その間に別のスレッドから参照が解放されてしまう可能性があるからです。


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

if (!wPtr.expired()) {
	std::shared_ptr<int> sPtrTmp = wPtr.lock();
	//マルチスレッドプログラムの場合、
	//expired関数がfalseを返したとしても、次の瞬間には
	//別スレッドから参照が解放されているかもしれない
	//sPtrTmpがポインタを管理した状態であることは保証されない
}

lock関数は参照の取得に失敗すれば空のshared_ptrを返すのでこれをチェックすればよく、上記のような確認処理はそもそも不要です。