この章の概要です。
std::shared_ptr は、スマートポインタの一種で、確保されたリソースを指すポインタを共有管理します。
第3章で取り上げた std::unique_ptr はリソースを独占所有するときに使いますが、std::shared_ptr は複数の箇所で共有財産として使用するときに使います。どちらかがより優れているということではないので、適切に使い分けるようにします。
std::shared_ptr は、<memory> という標準ヘッダで、以下のように定義されています。
namespace std {
template <typename T>
class shared_ptr;
}
テンプレート仮引数 T が、管理するポインタが指す型です。std::unique_ptr と比べて非常にシンプルです。
デリータの仕組みは std::shared_ptr にもありますが、テンプレート仮引数で指定する形ではなく、コンストラクタの実引数で渡す形になっています。デリータについては、項をあらためて取り上げます。
std::shared_ptr はリソースを共有管理します。具体的にいうと、同じリソースを管理している std::shared_ptr が複数存在できるということです。この点が、std::unique_ptr との直接的な違いです。
同じリソースを管理している複数の std::shared_ptr のうち、最後の1つが破棄されるときに、管理しているリソースの解放が実行されます。この不思議な能力は、参照カウントという方式で実装されています。参照カウントという仕組みは、std::shared_ptr に限らずさまざまな場面で活用できるものです。
参照カウントは、何かのきっかけでプラスされ、何かのきっかけでマイナスされる数値です。この数値を、どこかの変数(参照カウンタ)に保存しておきます。普通、初期状態を 0 としておき、何かのきっかけで +1、何かのきっかけで -1 するように実装します。多くの場合、0 が 1 になったときや、1 が 0 になったときに何らかの処理を実行させます。
std::shared_ptr の場合なら、初期状態を 0 としておき、これをリソースを管理していないことを表すというルールにしておきます。そして std::shared_ptr がリソースを管理しようとするたびに +1、std::shared_ptr がリソースを管理しなくなったときに -1 します。そして、参照カウンタの値が 1 から 0 になったときに解放処理を実行します。
実装上問題になるのは、参照カウンタ自体をどこに置けばいいのかという点です。同じリソースを管理する std::shared_ptr のオブジェクトは複数ある訳ですから、std::shared_ptr のメンバ変数^にはできません。すべての std::shared_ptr のオブジェクトからアクセスできて、かつ無駄にメモリを取らず、効率良く実装できる場所が求められます。
そこで通常、フリーストア(ヒープ領域)に参照カウンタを確保します(あるいは、アロケータを指定している場合は、それを用いて領域を確保します)。この確保は、あるリソースを管理する1つ目の std::shared_ptr が作られるとき、つまり、参照カウンタの値が 0 から 1 になるときに行われます。解放はその逆、参照カウンタの値が 1 から 0 になるときに行います。
【上級】なお、std::shared_ptr の参照カウンタの値を操作する際には、並列処理(【言語解説】第44章)でも問題がないように、同期制御が行われています。そのため、多少の処理負荷が発生します。同期制御が行われているのはこの部分だけであって、std::shared_ptr全体が、スレッドセーフになっているわけではありません。
また、現在の参照カウンタの値は、use_countメンバ関数を使って取得できます。
long use_count() const noexcept;
noexcept は例外を送出しないことを表すキーワードです(【言語解説】第18章)
std::shared_ptr のコンストラクタに、生のポインタを渡すと、そのポインタが指しているリソースの解放を std::shared_ptr に任せることになります。これは、std::unique_ptr と同じですが、std::shared_ptr の場合は、共有管理が可能なスマートポインタなので、独占的に管理するとは限りません。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::shared_ptr<MyClass> p(new MyClass());
}
実行結果:
Constructor
Destructor
std::shared_ptr のデフォルトコンストラクタは、何も管理していない状態(所有権を持たない状態)で初期化します。ヌルポインタを渡した場合も同様の状態になります。
std::shared_ptr のコピーコンストラクタは、コピー元と同じポインタを管理するように初期化され、参照カウントを +1 します。std::unique_ptr との特性の違いが端的に現れている場面と言えるかもしれません。
std::shared_ptr のムーブコンストラクタは、ムーブ元の std::shared_ptr はポインタを管理しなくなり、ムーブ先が管理を引き継ぎます。リソースの所有権は、ムーブ元が手放し、ムーブ先が引き継ぐので、参照カウントの値は変化しません。実際、参照カウントを操作するような処理は実行されません。
#include <iostream>
#include <memory>
#include <utility>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::shared_ptr<MyClass> p1(new MyClass());
std::cout << p1.use_count() << std::endl;
std::shared_ptr<MyClass> p2(p1);
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
std::shared_ptr<MyClass> p3(std::move(p1));
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
}
実行結果:
Constructor
1
2, 2
0, 2
Destructor
ところで、次のようなコードでは、参照カウントが 2 にはならず、問題があるコードだということをよく理解しておいてください。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
* c = new MyClass();
MyClass
std::shared_ptr<MyClass> p1(c);
std::shared_ptr<MyClass> p2(c);
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
}
2つの std::shared_ptr
は同じポインタを管理するようですが、これは個別に同じにポインタを管理してしまう間違った使い方になっています。2つの
std::shared_ptr
オブジェクト同士が何もやり取りを行っていないので、両者が同じポインタを管理していることを、std::shared_ptr
オブジェクトが知ることなどできません。
実際、use_count関数の呼び出しは2つとも 1
を返します。その後、main関数が終了するときに、p1 と p2
が破棄され、同じリソースを解放しようとして、二重解放となってしまいます。
正しく使うには、スマートポインタを使うときのセオリーに従い、new で得たポインタをいったん、ローカル変数に受け取ってからスマートポインタへ渡すようなコードを書かないようにしましょう。1つ目の std::shared_ptr にだけポインタを渡し、2つ目以降の std::shared_ptr はコピーコンストラクタで生成すれば良いです。また、後述する std::make_shared関数 も活用しましょう。
std::shared_ptr のデストラクタでは、デフォルトでは delete を使った解放を行います。この動作は、デリータの機能を使って変更できます。
std::unique_ptr と違い、std::shared_ptr は配列に対するサポートがありません。解放時に delete [] を使うデリータ(後述)を与えてやることで、配列として確保されたリソースを正しく解放することは可能ですが、[]演算子が使えないので、結局あまり役に立つものでもありません。
std::shared_ptr のコンストラクタには、std::unique_ptr を渡せるものがあり、所有権を std::shared_ptr の方へ移動できます。移動なので、std::move関数を使う必要があります。
ただし、std::unique_ptr側の管理するリソースの型(1つ目のテンプレート仮引数の型)から、std::shared_ptr が管理するリソースの型(こちらも、1つ目のテンプレート仮引数の型)へ変換できる必要はあります。
#include <iostream>
#include <memory>
#include <utility>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::unique_ptr<MyClass> up(new MyClass());
std::shared_ptr<MyClass> sp(std::move(up));
}
実行結果:
Constructor
Destructor
反対に、std::shared_ptr から std::unique_ptr へ変換できませんし、共有されている可能性を踏まえると困難だと言えます。そのため、リソースを共有せず、独占的な管理で良いのであれば、std::unique_ptr を優先的に使った方が良いでしょう。後から、std::shared_ptr に修正することは容易です。
管理している生のポインタは、getメンバ関数を使って取得できます。この関数は、ポインタを管理していないときには nullptr を返します。
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p1 = std::make_shared<int>(100);
int* rawPtr = p1.get();
std::cout << *rawPtr << std::endl;
std::shared_ptr<int> p2;
= p2.get();
rawPtr if (rawPtr == nullptr) {
std::cout << "null" << std::endl;
}
else {
std::cout << "not null" << std::endl;
}
}
実行結果:
100
null
std::shared_ptr は、生のポインタと同じように、*演算子や ->演算子を使えます。
たとえば、次のように書けます。
#include <iostream>
#include <memory>
class MyClass {
public:
explicit MyClass(int n) : mNum(n)
{}
inline void SetNum(int n)
{
= n;
mNum }
inline int GetNum() const
{
return mNum;
}
private:
int mNum;
};
int main()
{
std::shared_ptr<MyClass> p = std::make_shared<MyClass>(100);
const MyClass& c = *p;
->SetNum(200);
p
std::cout << p->GetNum() << std::endl;
std::cout << c.GetNum() << std::endl;
}
実行結果:
200
200
*演算子は、管理しているポインタが指す先にあるものを左辺値参照で返します。いわゆるポインタの間接参照を実現します。->演算子は、管理しているポインタを通して、指す先にあるものを操作するときに使います。
当然、ポインタを管理していないときにこれらの操作を行うと、未定義の動作となってしまうので注意してください。ポインタを管理しているかどうかを調べるには、getメンバ関数が nullptr を返さないことを確認するか、bool型への型変換演算子(【言語解説】第9章)を利用して、以下のように問い合わせます。
// std::shared_ptr の p がポインタを管理しているか?
if (p) {}
resetメンバ関数を使うと、管理対象のポインタを変更できます。もともと何らかのポインタを管理していた場合は、参照カウントが -1 されます。もちろん、0 になった場合は解放処理が実行されます。
また、実引数がない resetメンバ関数を呼ぶか、実引数に nullptr を指定すると、何も管理していない状態にできます。こちらも、もともと何らかのポインタを管理していた場合は、参照カウントが -1 されます。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> p2 = p1;
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
.reset(new MyClass());
p2
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
.reset();
p1
std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;
}
実行結果:
Constructor
2, 2
Constructor
1, 1
Destructor
0, 1
Destructor
ポインタ型をキャストするのと同様に、std::shared_ptr 自体をキャストできます。といってもキャスト構文が使えるわけではなく、専用の関数を使います。
static_cast に相当する std::static_pointer_cast関数、dynamic_cast に相当する std::dynamic_pointer_cast関数、const_cast に相当する std::const_pointer_cast関数 の3つがあります。
キャスト元の std::shared_ptr がポインタを管理していない場合は、キャスト後の std::shared_ptr もポインタを管理していない状態になります。また、std::dynamic_pointer_cast関数において、ダウンキャストに失敗したときも、ポインタを管理していない状態の std::shared_ptr が返されます。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
virtual ~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
class SubClass : public MyClass {};
int main()
{
std::shared_ptr<const MyClass> p1 = std::make_shared<SubClass>();
std::shared_ptr<const MyClass> p2 = std::static_pointer_cast<const MyClass>(p1);
std::shared_ptr<MyClass> p3 = std::const_pointer_cast<MyClass>(p2);
std::shared_ptr<SubClass> p4 = std::dynamic_pointer_cast<SubClass>(p3);
if (p4) {
std::cout << "dynamic_pointer_cast() -- OK" << std::endl;
}
else {
std::cout << "dynamic_pointer_cast() -- Failed" << std::endl;
}
std::cout << p1.use_count() << ", "
<< p2.use_count() << ", "
<< p3.use_count() << ", "
<< p4.use_count() << std::endl;
}
実行結果:
Constructor
dynamic_pointer_cast() -- OK
4, 4, 4, 4
Destructor
キャストしているので、当然、異なる型の std::shared_ptr ができあがる訳ですが、きちんと共有管理が行われます。つまり、キャスト前後で std::shared_ptr が持つ参照カウンタは同一です。キャストしたとはいえ、管理しているリソースは同じものですから、このような挙動になります。
std::unique_ptr と同様に、解放処理の部分をデフォルト以外の動作に置き換えることが可能になっています。これはデリータという仕組みを使って置き換えますが、その方法は std::unique_ptr とは異なっています。
std::unique_ptr のデリータが、テンプレート実引数で指定するようになっているのに対し、std::shared_ptr のデリータは、コンストラクタの実引数で渡すようになっています。
template <typename Y, typename Deleter>
(Y* ptr, Deleter d);
shared_ptr
template <typename Deleter>
(std::nullptr_t ptr, Deleter d); shared_ptr
第2引数でデリータの指定を行います。引数にデリータの指定がないコンストラクタを使った場合は、デフォルトのデリータである std::default_delete が使われます。
デリータを指定する場合は、std::make_shared関数を使うことはできません。一応、後述する std::allocate_shared関数 でデリータを指定できますが、この関数の本来の目的は別のところにあります。
デリータを指定する例を挙げておきます。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
void MyDeleter(MyClass* p)
{
std::cout << "call MyDeleter" << std::endl;
delete p;
}
int main()
{
std::shared_ptr<MyClass> p1(new MyClass(), MyDeleter);
std::shared_ptr<MyClass> p2(p1);
}
実行結果:
Constructor
call MyDeleter
Destructor
std::shared_ptr のコピーを作る場合、コピー元のデリータが引き継がれます。このサンプルプログラムでは、p1 にしかデリータの指定はありませんが、p2 も同じデリータを共有していることになります。
デリータは、関数だけでなく、関数オブジェクト(【言語解説】第32章)やラムダ式(【言語解説】第32章)を使って指定することも可能です。
ラムダ式を使うと次のようになります。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::shared_ptr<MyClass> p1(new MyClass(),
[](MyClass* p) {
std::cout << "call MyDeleter" << std::endl;
delete p;
}
);
std::shared_ptr<MyClass> p2(p1);
}
実行結果:
Constructor
call MyDeleter
Destructor
std::shared_ptr のオブジェクトを生成する際に、参照カウンタのためのメモリ領域が確保されますが、この確保(と解放)をどのように行うかを指定できます。これは、アロケータと呼ばれるオブジェクトを渡すことで指定できます。
アロケータは、std::shared_ptr のコンストラクタの実引数から渡せるようになっています。
template <typename Y, typename Deleter, typename Alloc>
(Y* ptr, Deleter d, Alloc alloc);
shared_ptr
template <typename Deleter, typename Alloc>
(std::nullptr_t ptr, Deleter d, Alloc alloc); shared_ptr
第3引数がアロケータの指定です。アロケータを指定する引数がないコンストラクタを使った場合は、std::allocator が使われます。std::allocator は、標準ライブラリの各所で使われており、標準的な方法でメモリ確保と解放を行います。
また、どちらのコンストラクタも、第2引数にデリータの指定が必要なので、デリータはデフォルトのままで良いのであれば、std::default_delete を指定します。
アロケータという仕組みの詳細については、第25章で取り上げますが、一応、使い方だけ示しておくと、以下のようになります。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
std::cout << "Constructor" << std::endl;
}
~MyClass()
{
std::cout << "Destructor" << std::endl;
}
};
int main()
{
std::allocator<MyClass> myAlloc;
std::shared_ptr<MyClass> p(new MyClass(), std::default_delete<MyClass>(), myAlloc);
}
実行結果:
Constructor
Destructor
この例では、デリータもアロケータも、デフォルトと同じものを明示的に指定しているだけなので、特に動作に変化はありません。
std::shared_ptr で管理されているオブジェクトが、自分自身を指すポインタを std::shared_ptr として持ちたいことがあります。たとえば次のようなプログラムを考えます。
#include <iostream>
#include <memory>
class MyClass {
public:
()
MyClass{
= msPointerCount++;
mIndex [mIndex] = std::shared_ptr<MyClass>(this);
msPointers}
~MyClass()
{
[mIndex].reset();
msPointers}
private:
int mIndex;
private:
static std::shared_ptr<MyClass> msPointers[16];
static int msPointerCount;
};
std::shared_ptr<MyClass> MyClass::msPointers[16];
int MyClass::msPointerCount;
int main()
{
std::shared_ptr<MyClass> p = std::make_shared<MyClass>();
}
MyClass のオブジェクトが作られるたびに、そのポインタを配列にリストアップしておき、破棄されるときに取り除くとします。つまり、存在するオブジェクトをつねに1か所で管理するということです。
static指定子が付いたメンバについては、【言語解説】第19章で説明しています。
問題は、MyClass のコンストラクタの中で行っている、次の1文です。
[mIndex] = std::shared_ptr<MyClass>(this); msPointers
自分自身をリストへ登録するため、thisポインタを std::shared_ptr へコピー代入したいのです。 そのために std::shared_ptr のコンストラクタに thisポインタを渡していますが、ここに問題があります。
これが問題なのは、「新たな」std::shared_ptr を作ってしまっていることです。thisポインタが指し示しているオブジェクトは、main() で std::make_shared関数によって生成したものです。つまり、main関数の側にある std::shared_ptr がすでに所有権を持っているので、本当に必要な処理は、「新規の」std::shared_ptr を作ることではなく、「同じリソースを共有管理する」std::shared_ptr を作ることなのです。そうでないと、2つの std::shared_ptr が同じリソースを「別個に」管理してしまうため、最終的に二重解放となってしまいます。
このように、std::shared_ptr で管理されるオブジェクトを指す thisポインタを std::shared_ptr で扱いたいときには、 std::enable_shared_from_this というクラステンプレートから派生させるようにします。テンプレート仮引数には、派生クラスの型を指定します。
具体的には、次のようなプログラムになります。いくつか注意しなければならない点があるので、コメントで補足しています。
#include <iostream>
#include <memory>
class MyClass : public std::enable_shared_from_this<MyClass> {
private:
// 必ず std::shared_ptr で管理させるため、
// コンストラクタは「非公開」にし、専用の初期化関数を使わせる。
() {}
MyClass
public:
~MyClass()
{
[mIndex].reset();
msPointers}
public:
// 必ず std::shared_ptr で管理させるため、専用関数を作る。
static std::shared_ptr<MyClass> Create()
{
// std::make_shared() の中でインスタンス化を行うには、
// 「公開」されたコンストラクタが必要。
// そこで、派生クラスを用意して、派生クラス型で生成させる。
// ただし、その結果は基底クラスの型で受け取り、
// 以降はすべて基底クラスの型で操作する。
class Helper : public MyClass {};
std::shared_ptr<MyClass> p = std::make_shared<Helper>();
// インスタンス化を終えてからでないと、shared_from_this() が機能しないので、
// コンストラクタの後で呼ぶ。
->Initialize();
p
return p;
}
private:
void Initialize()
{
= msPointerCount++;
mIndex [mIndex] = shared_from_this();
msPointers}
private:
int mIndex;
private:
static std::shared_ptr<MyClass> msPointers[16];
static int msPointerCount;
};
std::shared_ptr<MyClass> MyClass::msPointers[16];
int MyClass::msPointerCount;
int main()
{
std::shared_ptr<MyClass> p = MyClass::Create();
}
実行結果:
std::enable_shared_from_thisクラステンプレートから派生したクラスは、std::shared_ptr で管理するようにインスタンス化しなければなりません。これは単に注意するというよりも、きちんと対策を講じるべきです。なぜなら、クラスを定義したプログラマーと、このクラスを使用するプログラマーが同じ人とは限らないからです。
そこで、MyClassクラスのコンストラクタは「非公開」とし、専用の初期化関数を用意します。そして、std::shared_ptr を作って返してやるようにするのです。
初期化関数内で std::shared_ptr を生成するとき、MyClass のコンストラクタが呼び出さなければなりません。new で生成して、std::shared_ptr のコンストラクタに引き渡す方法ならば問題ありませんが、std::make_shared関数を使うのであれば、MyClass のコンストラクタを「非公開」としていることが足かになってしまいます。
つまり、new MyClass
という部分が、std::make_shared関数の内部にある訳ですから、MyClass
にとっては他人なのです。したがって、MyClass
の「公開」されたコンストラクタが必要です。
対策としては、std::make_shared関数を諦めるか、このサンプルプログラムのように、派生クラス(【言語解説】第35章)を使ったトリック(?)を用います。std::make_shared関数を使う方が利点が多いので、後者の手段をとることになるでしょう。
std::enable_shared_from_thisクラステンプレートには、shared_from_this というメンバ関数があり、これを呼び出すと、thisポインタを指す適切な std::shared_ptr が返されます。「適切な」というのは、「同じリソースを共有管理する」std::shared_ptr の参照カウントを +1 したものということです。
shared_from_thisメンバ関数は、std::enable_shared_from_thisクラステンプレートのコンストラクタを呼び出した後でないと正しく機能しません。
問題① std::unique_ptr と std::shared_ptr は、どのような方針で使い分けますか?
問題② 次のように宣言された3つの関数があります。
void f1(std::shared_ptr<MyClass> p);
void f2(const std::shared_ptr<MyClass>& p);
void f3(std::shared_ptr<MyClass>&& p);
それぞれの関数に、std::shared_ptr のオブジェクトを渡すと、どのような処理が行われるか説明してください。
「VisualC++」という表現を「VisualStudio」に統一。
Xcode 8.3.3 を clang 5.0.0 に置き換え。
「thisポインタを共有する」の項を追加。
新規作成。
Programming Place Plus のトップページへ
はてなブックマーク に保存 | Pocket に保存 | Facebook でシェア |
X で ポスト/フォロー | LINE で送る | noteで書く |
RSS | 管理者情報 | プライバシーポリシー |