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

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要

この章の概要です。


概要

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_countメンバ関数は、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

リンク切れを判定する

冒頭で説明したように、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 では行えません。

循環参照

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 を使うことに、どのような利点がありますか?


解答ページはこちら

参考リンク


更新履歴

’2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。

’2017/11/1 新規作成。



前の章へ (第4章 shared_ptr)

次の章へ (第6章 vector)

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る