Modern C++編【標準ライブラリ】 第4章 shared_ptr

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

概要

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()
{
    MyClass* c = new 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::ahared_ptr は配列に対するサポートがありません。解放時に delete [] を使うデリータ(後述)を与えてやることで、配列として確保されたリソースを正しく解放することは可能ですが、[]演算子が使えないので、結局あまり役に立つものでもありません。

C++17 (配列への対応)

C++17 になって、std::ahared_ptr でも配列を扱うことができるようになりました。デフォルトでは、delete [] で解放されます。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::shared_ptr<int[]> pa(new int[5]);
    pa[0] = 10;
    std::cout << pa[0] << std::endl;
}

実行結果:

10

std::shared_ptr のテンプレートパラメータに配列型を指定した場合に限って、operator[] が定義され、[]演算子が使えます。いつものように、範囲外アクセスになるときの動作は未定義です。

この機能は VisualC++、Xcode ともに対応されていません。

std::unique_ptr からの生成

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 に修正することは容易です。

std::make_shared

あるリソースを管理する最初の std::shared_ptr のオブジェクトを作成するときには、std::make_shared() を使うと良いです。

std::make_shared() は関数テンプレートになっており、std::shared_ptr に管理してもらうオブジェクトを関数内で生成し、参照カウントが 1 の状態の std::shared_ptr を生成して返します。

std::make_shared() は、次のように宣言されています。

namespace std {
    template <typename T, typename... Args>
    shared_ptr<T> make_shared(Args&&... args);
}

生成するオブジェクト(std::shared_ptr に管理してもらうオブジェクト)のコンストラクタに渡す実引数を、std::make_shared() の実引数に指定します。可変個引数テンプレート(【言語解説】第30章)を使っているため、引数を何個でもどんな型でも渡すことができます。

std::make_shared() を使ったプログラムは、以下のようになります。

#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 = std::make_shared<MyClass>();
}

実行結果:

Constructor
Destructor

std::make_shared() を使うことで、我々が自分で書いた部分に new が登場しなくなります。std::make_shared() の内側に隠された訳です。このおかげで、スマートポインタの管理下に置かれていない瞬間ができることが無くなり、解放をし損なうことを避けられ、安全性が高まります。

また、std::make_shared() を使った方が効率面でも優れています。std::make_shared() を使わない方法では、リソースを生成するための new と、参照カウンタを生成するための new を別個に行わざるを得ません。しかし、std::make_shared() は、この2つの new をともに関数内で行えるため、1回の new にまとめて行うような最適化されたコードで実装されるのが普通です。

残念ながら、std::make_shared() はデリータを指定することができないので、デリータを指定しなければならない場合には諦める必要があります。

なお、テンプレート実引数が std::make_shared() を呼び出すときに指定済みですから、戻り値を受け取る変数の型で同じことを繰り返さず、auto(【言語解説】第20章)を使うのが良いです。

auto p = std::make_shared<MyClass>();

ポインタを取得する

管理している生のポインタは、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;
    rawPtr = p2.get();
    if (rawPtr == nullptr) {
        std::cout << "null" << std::endl;
    }
    else {
        std::cout << "not null" << std::endl;
    }
}

実行結果:

100
null

ポインタ操作

std::shared_ptr は、生のポインタと同じように、*演算子や ->演算子を使うことができます。

C++17 (配列版でのポインタ操作)

C++17 では、配列を扱う std::shared_ptr においては、*演算子や ->演算子は定義されないか、定義されていても事実上使うことができません(未定義の動作)。

例えば、次のように書けます。

#include <iostream>
#include <memory>

class MyClass {
public:
    explicit MyClass(int n) : mNum(n)
    {}

    inline void SetNum(int n)
    {
        mNum = n;
    }
    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;
    p->SetNum(200);

    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;

    p2.reset(new MyClass());

    std::cout << p1.use_count() << ", " << p2.use_count() << std::endl;

    p1.reset();

    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(【言語解説】第40章)に相当する 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 が持つ参照カウンタは同一のものです。キャストしたとはいえ、管理しているリソースは同じものですから、このような挙動になります。

C++17 (std::reinterpret_pointer_cast())

C++17 では、reinterpret_cast に対応した std::reinterpret_pointer_cast() が追加されています。

#include <iostream>
#include <memory>

int main()
{
    std::shared_ptr<int> p1 = std::make_shared<int>(100);

    std::shared_ptr<double> p2 = std::reinterpret_pointer_cast<double>(p1);

    std::cout << p1.use_count() << ", "
              << p2.use_count() << std::endl;
}

実行結果:

2, 2

std::reinterpret_pointer_cast() は、VisualC++、Xcode ともに対応していません。

デリータ

std::unique_ptr と同様、解放処理の部分をデフォルト以外の動作に置き換えることが可能になっています。これはデリータという仕組みを使って置き換えますが、その方法は std::unique_ptr とは異なっています。

std::unique_ptr のデリータが、テンプレート実引数で指定するようになっているのに対し、std::shared_ptr のデリータは、コンストラクタの実引数で渡すようになっています。

template <typename Y, typename Deleter>
shared_ptr(Y* ptr, Deleter d);

template <typename Deleter>
shared_ptr(std::nullptr_t ptr, Deleter d);

第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>
shared_ptr(Y* ptr, Deleter d, Alloc alloc);

template <typename Deleter, typename Alloc>
shared_ptr(std::nullptr_t ptr, Deleter d, Alloc alloc);

第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::allocate_shared()

アロケータを使う場合は、std::allocate_shared() というヘルパー関数を使うことができます。

namespace std {
    template<typename T, typename Alloc, typename... Args>
    shared_ptr<T> allocate_shared(const Alloc& alloc, Args&&... gs);
}

std::make_shared() にアロケータの指定を追加したものです。std::make_shared() のところで取り上げた通り、管理するリソースの生成の他に、参照カウンタの生成もまとめて行うように実装されています。std::allocate_shared() の場合は、アロケータを使って、まとめてメモリ確保を行います。

普通にコンストラクタを使うより std::make_shared() を優先するのと同様、コンストラクタでアロケータの指定を行うより std::allocate_shared() を優先して使う方が良いでしょう。std::make_shared() と同じ理由から安全性が向上でき、やはり同じ理由から効率面でも優れています。

#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 = std::allocate_shared<MyClass>(myAlloc);
}

実行結果:

Constructor
Destructor

やはり、auto(【言語解説】第20章)を使った方が記述がすっきりします。

auto p = std::allocate_shared<MyClass>(myAlloc);

thisポインタを共有する

std::shared_ptr で管理されているオブジェクトが、自分自身を指すポインタを std::shared_ptr として持ちたいことがあります。例えば次のようなプログラムを考えます。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass()
    {
        mIndex = msPointerCount++;
        msPointers[mIndex] = std::shared_ptr<MyClass>(this);
    }
    ~MyClass()
    {
        msPointers[mIndex].reset();
    }

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文です。

msPointers[mIndex] = std::shared_ptr<MyClass>(this);

自分自身をリストへ登録するため、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 というクラステンプレートから派生(【言語解説】第34章)させるようにします。テンプレートパラメータには、派生クラスの型を指定します。

具体的には、次のようなプログラムになります。幾つか注意しなければならない点があるので、コメントで補足しています。

#include <iostream>
#include <memory>

class MyClass : public std::enable_shared_from_this<MyClass> {
private:
    // 必ず std::shared_ptr で管理させるため、
    // コンストラクタは「非公開」にし、専用の初期化関数を使わせる。
    MyClass() {}

public:
    ~MyClass()
    {
        msPointers[mIndex].reset();
    }

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() が機能しないので、
        // コンストラクタの後で呼ぶ。
        p->Initialize();

        return p;
    }

private:
    void Initialize()
    {
        mIndex = msPointerCount++;
        msPointers[mIndex] = shared_from_this();
    }

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 のオブジェクトを渡すと、どのような処理が行われるか説明して下さい。


解答ページはこちら

参考リンク

更新履歴

'2017/11/1 「thisポインタを共有する」の項を追加。

'2017/10/24 新規作成。



前の章へ

次の章へ

Modern C++編のトップページへ

Programming Place Plus のトップページへ