スマートポインタ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_ptr
はC++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_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;
}
//この時点でptr1とptr2にアクセス不可能になるが
//メモリは解放されていない
std::cin.get();
}
コンストラクタ コンストラクタ
コンストラクタの実行は確認できますが、デストラクタは一度も呼ばれていません。
shared_ptr
のインスタンス同士でお互いを所有した状態にすると、メモリがいつまで経っても解放されなくなります。
これはつまり、
- まず
ptr1
の寿命が尽きる
ptr2
がptr1
を所有しているので、deleteされない
- 次に
ptr2
の寿命が尽きる
ptr1
がptr2
を所有しているので、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_ptr
はshared_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_ptr
のreset
関数は参照を解放するだけで、他の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
を返すのでこれをチェックすればよく、上記のような確認処理はそもそも不要です。