Modern C++編【標準ライブラリ】 第5章 weak_ptr

先頭へ戻る

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

この章の概要

この章の概要です。

概要

std::weak_ptr は少々特殊なスマートポインタで、単独で使われることはなく、std::shared_ptrとセットで使用します。そうして、std::shared_ptr だけでは解決できない問題(後の項で取り上げます)に対処します。
そのため、まずは std::shared_ptr を理解することが必要です。理解が足りないと感じるようなら、第4章を参照して下さい。

std::weak_ptr は、memory という標準ヘッダで、以下のように定義されています。

namespace std {
    template <typename T>
    class weak_ptr;
}

std::weak_ptr は、std::shared_ptr と同様に、共有されるリソースを指すポインタを持ちますが、参照カウンタの値を増減しないという点が違います。weak_ptr の「weak(弱い)」とは、この点を表しています。

std::weak_ptr の本質は、「他人が所有しているリソースへの参照」ということになります。自分のものではないから、参照カウンタの値を増減しないということです。

std::shared_ptr が管理しているリソースを、std::weak_ptr から参照するということですが、自分のものではないのだから、知らないうちにリソースが解放されている可能性があることを考慮しなければなりません。

「他のすべての std::shared_ptr が破棄されたときに、リソースの解放処理を実行する "可能性があります"」としたように、このタイミングでは解放処理を実行しないこともあります。この件については、「生存数の管理」で取り上げます。

生存数の管理

std::shared_ptr は参照カウンタを管理しているのでした。実際には、それ以外の情報もセットで管理しています。その中でも特に、std::weak_ptr の生存数を数える、weak参照カウンタの存在が重要です。なぜこんなものが必要なのでしょうか。

std::weak_ptr が、自分が参照しているリソースがまだ存在しているかどうかを知るには、参照カウンタの値を確認する必要があります。その参照カウンタがあるのは、std::shared_ptr の側です。

一方、std::shared_ptr は、参照カウンタの値が 0 になったときに、リソースの解放処理を実行します。そのまま、参照カウンタ自体も解放してしまいたいですが、std::weak_ptr が参照カウンタの値を調べにくる可能性を考慮しなければなりません。
そのため、参照カウンタの解放は、std::weak_ptr が存在している限りは先送りしなければならないのです。そこで、std::weak_ptr の生存数を把握しておくようになっており、それは参照カウンタとセットで同じ場所に置かれている訳です。

第4章で、std::shared_ptr を生成する方法として、普通にコンストラクタを使う方法と、std::make_shared() のようなヘルパー関数を使う方法とを取り上げました。後者の長所として、リソース自体の new と、参照カウンタの new とをひとまとめにして効率の向上が図られている点を上げましたが、std::weak_ptr が絡むと、解放に関してはかえって問題があるかも知れません。
つまり、ヘルパー関数を使った場合、リソースと参照カウンタと weak参照カウンタとが、すべて1回の new で行われているため、別個に delete することができません。そのため、std::wake_ptr が生き残っている限り、リソースそのものも解放できないということになります。参照カウンタに影響しないからといって、いつまでも std::weak_ptr を生かしておくと、メモリがいつまでも使われ続ける恐れがあります。

初期化と破棄

std::weak_ptr は幾つかのコンストラクタを持っていますが、基本的には、std::shared_ptr の const参照を実引数に取るタイプを使うことになります。

template <typename Y>
weak_ptr(const shared_ptr<Y>& r) noexcept;

このコンストラクタは、指定した std::shared_ptr と同じリソースを指すように初期化します。前述している通り、このとき参照カウンタを増加させませんが、weak参照カウンタは増加します。

具体的なプログラム例は、次のようになります。

#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::cout << p1.use_count() << std::endl;

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

実行結果:

Constructor
1
1, 1
Destructor

std::weak_ptr のコンストラクタに std::shared_ptr のオブジェクトを渡しても、use_count() が返す値が変化していないことを確認して下さい。
なお、use_counr() は、std::shared_ptr にも std::weak_ptr にも用意されています。どちらも参照カウンタの値を返す、同じ意味合いの関数です。

std::weak_ptr のデストラクタでは、参照カウンタの値は減りませんが、weak参照カウンタは減ります。weak参照カウンタの値を調べる標準的な手段はありません。

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

    {
        std::weak_ptr<MyClass> wp(sp);
        std::cout << wp.use_count() << std::endl;
        std::cout << "destroy weak_ptr" << std::endl;
    }
    std::cout << sp.use_count() << std::endl;
}

実行結果:

Constructor
1
destroy weak_ptr
1
Destructor

C++14 (ムーブコンストラクタ)

C++14 で、std::weak_ptr の右辺値参照を引数にとるムーブコンストラクタが追加されました。

ムーブ元の std::weak_ptr は何も管理していない状態に戻り、ムーブ先に引き継がれます。参照カウンタも、weak参照カウンタも変化しないので、高速に実行できます。

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

    std::weak_ptr<MyClass> wp1 = sp;
    std::weak_ptr<MyClass> wp2 = std::move(wp1);

    std::cout << wp1.use_count() << std::endl;
    std::cout << wp2.use_count() << std::endl;
}

実行結果:

Constructor
0
1
Destructor

VisualC++、Xcode ともに、この機能に対応しています。

リンク切れを判定する

冒頭で説明したように、std::weak_ptr は参照カウンタを増やさないので、まだ std::weak_ptr のオブジェクトが残っていても、リソースの方が解放されてしまうことがあります。この状況を、ここではリンクが切れていると表現することにします。

std::weak_ptr の側からリソースへアクセスしたいときは、リンク切れを起こしていないかどうかに注意しなければなりません。expiredメンバ関数を使えば、リンク切れしているかどうかを確認することができます。この関数は、リンク切れしていたら true を、していなければ false を返します。

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

    std::cout << wp.expired() << std::endl;
    sp.reset();
    std::cout << wp.expired() << std::endl;
}

実行結果:

Constructor
0
Destructor
1

リンクを切る

resetメンバ関数を使うと、リンクを切って、何も参照していない状態に戻すことができます。

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

    std::cout << wp.expired() << std::endl;
    wp.reset();
    std::cout << wp.expired() << std::endl;
}

実行結果:

Constructor
0
1
Destructor

std::unique_ptr や std::shared_ptr の resetメンバ関数と違って、新たなポインタを再設定する機能はありません。

リソースへのアクセス

std::weak_ptr のメンバ関数の一覧を眺めると、一番疑問に思うことは、*演算子が無いことでしょう。実は、std::weak_ptr は、直接的にリソースへアクセスすることができません。

というのも、std::weak_ptr が参照するリソースは他人の所有物なので、いつ消えてしまうか分からないのでした。そこで「これから参照するけれど、まだそこにありますか?」という問い合わせと、「これから参照するので、まだ消さないでください」というお願いをする必要があります。これを忘れず確実に行えるようにするため、std::weak_ptr からリソースへアクセスするための方法がきちんと用意されています。

もう少し具体的なことを言うと、std::weak_ptr を使って、参照先のリソースを共有する std::shared_ptr のオブジェクトを生成するのです。そうすれば、参照カウンタが増えるので、使っている間はリソースが消えないことが保証されます。使い終わったら、生成した std::shared_ptr のオブジェクトを黙って破棄すればよいです。参照カウンタが減るので、使い終わったことがきちんと伝わります。

std::weak_ptr から std::shared_ptr を作る方法は2つあります。

1つは、std::shared_ptr のコンストラクタに std::weak_ptr の const参照を渡してやることです。この場合、std::weak_ptr のリンクが切れていなければ、std::weak_ptr が指しているリソースと同じリソースを指す std::shared_ptr のオブジェクトが生成できます。
もし、リンクが切れていたら、つまりリソースが消えてしまっていたら、std::bad_weak_ptr例外(第12章)が送出されます。

#include <iostream>
#include <memory>

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

    void Func()
    {
        std::cout << "Func" << std::endl;
    }
};

void CallFunc(const std::weak_ptr<MyClass>& wp)
{
    try {
        std::shared_ptr<MyClass> sp(wp);
        sp->Func();
    }
    catch (const std::bad_weak_ptr& ex) {
        std::cout << ex.what() << std::endl;
    }
}

int main()
{
    std::shared_ptr<MyClass> sp = std::make_shared<MyClass>();

    std::weak_ptr<MyClass> wp = sp;
    CallFunc(wp);

    sp.reset();
    CallFunc(wp);
}

実行結果:

Constructor
Func
Destructor
bad_weak_ptr

もう1つの方法は、std::weak_ptr の lockメンバ関数を使うことです。こちらは、戻り値で std::shared_ptr のオブジェクトを返します。もし、std::weak_ptr がリンク切れになっていたら、つまりリソースが消えてしまっていたら、所有権を持っていない std::shared_ptr が返されます。

#include <iostream>
#include <memory>

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

    void Func()
    {
        std::cout << "Func" << std::endl;
    }
};

void CallFunc(const std::weak_ptr<MyClass>& wp)
{
    std::shared_ptr<MyClass> sp = wp.lock();
    if (sp) {
        sp->Func();
    }
    else {
        std::cout << "null" << std::endl;
    }
}

int main()
{
    std::shared_ptr<MyClass> sp = std::make_shared<MyClass>();

    std::weak_ptr<MyClass> wp = sp;
    CallFunc(wp);

    sp.reset();
    CallFunc(wp);
}

実行結果:

Constructor
Func
Destructor
null

コピーとムーブ

std::weak_ptr のオブジェクトはコピーすることができます。コピーが出来ても参照カウンタには影響を与えませんが、weak参照カウンタは増加します。

また、std::shared_ptr をコピー元にすることもでき、コピー元と同じリソースを指すポインタを所有する std::weak_ptr になります。参照カウンタの値は変化せず、weak参照カウンタが増加します。

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

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

    std::weak_ptr<MyClass> wp1, wp2;
    wp1 = sp;    // std::shared_ptr からコピー
    std::cout << sp.use_count() << ", " << wp1.use_count() << std::endl;

    wp2 = wp1;   // std::weak_ptr をコピー
    std::cout << wp1.use_count() << ", " << wp2.use_count() << std::endl;
}

実行結果:

Constructor
1
1, 1
1, 1
Destructor

ムーブに関しては、C++11 では行えません。

C++14 (ムーブ)

C++14 で、std::weak_ptr から std::weak_ptr へのムーブ代入が可能になりました。

ムーブ元の std::weak_ptr は何も管理していない状態に戻り、ムーブ先に引き継がれます。参照カウンタも、weak参照カウンタも変化しないので、高速に実行できます。

#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> sp = std::make_shared<MyClass>();
    std::weak_ptr<MyClass> wp1, wp2;

    wp1 = sp;              // std::shared_ptr からコピー
    wp2 = std::move(wp1);  // std::weak_ptr をムーブ

    std::cout << wp1.expired() << std::endl;
    std::cout << wp2.expired() << std::endl;
}

実行結果:

Constructor
1
0
Destructor

VisualC++、Xcode はともに、この機能に対応しています。

循環参照

std::weak_ptr を使う理由としてもう1つ、std::shared_ptr では解決できない循環参照の問題に対応するというものがあります。

循環参照というのは、AがBを std::shared_ptr によって管理し、反対にBがAを std::shared_ptr によって管理しているような、相互に参照しあう関係性のことです。例として、次のサンプルプログラムを見てみましょう。

#include <iostream>
#include <memory>

// クラスの前方宣言(【言語解説】第25章
class A;
class B;

class A {
public:
    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    void SetB(const std::shared_ptr<B>& b)
    {
        mB = b;
    }

private:
    std::shared_ptr<B> mB;
};

class B {
public:
    ~B()
    {
        std::cout << "~B()" << std::endl;
    }

    void SetA(const std::shared_ptr<A>& a)
    {
        mA = a;
    }

private:
    std::shared_ptr<A> mA;
};

int main()
{
    std::shared_ptr<A> a = std::make_shared<A>();  // A のオブジェクト(以後ObjA)を生成し、a に所有させる
    std::shared_ptr<B> b = std::make_shared<B>();  // B のオブジェクト(以後ObjB)を生成し、b に所有させる

    a->SetB(b);  // a->mB が objB を所有(b の参照カウントが増える)
    b->SetA(a);  // b->mA が objA を所有(a の参照カウントが増える)

    a.reset();   // a は objA の所有権を手放して参照カウントが減るが、
                 // まだ b->mA があるので 0 とはならず、objA は解放されない。
    b.reset();   // b は objB の所有権を手放して参照カウントが減るが、
                 // まだ objA が生きているので objA->mB があるため 0 とはならず、
                 // objB は解放されない。
}
// main() の終わりで解体されるのは a と b だが、
// a も b も既に何も所有していないため、実質的に何も起こらない。
// 結局、objA と objB を解放してくれるものは誰もいない。

実行結果:




このプログラムを実行してみると、何も出力されません。クラスA、B のデストラクタには、標準出力へメッセージを出力する文が含まれているのにも関わらずです。つまり、A も B も、そのオブジェクトは解放されることなく、プログラムが終了してしまっています。

感覚的には分かるようでも、具体的な理解は意外とややこしいので、コメントを参考にコードをよく読んで頂きたいと思います。

このような問題を解決する手段の1つが、std::weak_ptr を使うことです。互いが互いの存在に本当に依存しているのであれば、std::weak_ptr を使う訳にはいきませんが、相手が存在しているのならば参照するという緩い(弱い)関係性なのであれば、std::weak_ptr を使うべきです。

必要なのは、クラスA と B のメンバ変数を std::weak_ptr に変えることだけです。

#include <iostream>
#include <memory>

// クラスの前方宣言(【言語解説】第25章
class A;
class B;

class A {
public:
    ~A()
    {
        std::cout << "~A()" << std::endl;
    }

    void SetB(const std::shared_ptr<B>& b)
    {
        mB = b;
    }

private:
    std::weak_ptr<B> mB;
};

class B {
public:
    ~B()
    {
        std::cout << "~B()" << std::endl;
    }

    void SetA(const std::shared_ptr<A>& a)
    {
        mA = a;
    }

private:
    std::weak_ptr<A> mA;
};

int main()
{
    std::shared_ptr<A> a = std::make_shared<A>();  // A のオブジェクト(以後ObjA)を生成し、a に所有させる
    std::shared_ptr<B> b = std::make_shared<B>();  // B のオブジェクト(以後ObjB)を生成し、b に所有させる

    a->SetB(b);  // a->mB が objB を参照する(b の参照カウントは増えない)
    b->SetA(a);  // b->mA が objA を参照する(a の参照カウントは増えない)

    a.reset();   // a は objA の所有権を手放して参照カウントが減る。
                 // 参照カウントが 0 になるので、objA は解放される。
    b.reset();   // b は objB の所有権を手放して参照カウントが減る。
                 // 参照カウントが 0 になるので、objB は解放される。
}
// main() の終わりで解体されるのは a と b だが、
// a も b も既に何も所有していないため、実質的に何も起こらない。
// objA、objB は reset() のときに解放済みである。

実行結果:

~A()
~B()

実行結果の通り、クラスA、B のデストラクタが呼び出されているようです。

メンバ変数が std::weak_ptr に変わったことで、SetA()、SetB() を呼び出したときに、参照カウンタが増えなくなっていることが最大のポイントです。おかげで、reset() のところでスムーズに解放が行われ、何も問題になる部分がありません。


練習問題

問題① 参照カウンタの値を増減させないのであれば、std::weak_ptr ではなく、生のポインタを使ってはいけないのでしょうか? std::weak_ptr を使うことに、どのような利点がありますか?


解答ページはこちら

参考リンク

更新履歴

'2017/11/1 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ