LoginSignup
6
5

More than 5 years have passed since last update.

【no more クラッシュ】thisをbindするのは危険!weak_ptrでやろう。

Last updated at Posted at 2018-01-15

C++でObserverパターン(何か起こったら通知してもらうやつ)を実装する場合、関数オブジェクトとして、ハンドラを登録しておく方法を取ることが多いと思います。

そんな時、「自分のこのメソッドを呼んで!」という意味で、

AddHandler( std::bind(&HogeClass::HogeMethod, this) );

こんな感じで書いたりしていませんか? でもちょっと待って!このthisは、ポインタ値としてハンドラ側に保持されます。
実際にハンドラが呼ばれる際に、有効かどうかわかりません!

こんな時はどうしたら良いのか、解説します。
まずはNGパターンから見てみましょう。

#include <iostream>
#include <vector>
#include <functional>
#include <memory>

using namespace std;

//ハンドラを呼ぶ側
class Subject {
    std::vector<std::function<void()>> m_handlers;

private:

    //ハンドラを呼ぶ
    void Notify() {
        for( auto& o : m_handlers ) {
            o();
        }
    }

public:
    //ハンドラを登録
    void AddHandler(std::function<void()> handler) {
        m_handlers.push_back(handler);
    }

    void Update() {
        //if( hoge ) {    //何かのイベントが発生したら、Notifyする
            Notify();
        //}
    }
};


//ハンドラ呼ばれる側
class Observer : public std::enable_shared_from_this<Observer>
{
public:
    Observer() {
    }
    virtual ~Observer() {
        cout << "destructed" << endl;
    }
    void Setup( Subject& sbj ) {
        sbj.AddHandler(std::bind(&Observer::OnNotify1, this));

    }
    void OnNotify1() {
        cout << "notified" << endl;
    }
};

int main() {
    Subject sbj;
    {
        //Observerインスタンスは実際は別の場所で管理されている想定
        auto obs = std::make_shared<Observer>();
        obs->Setup(sbj);
        sbj.Update();
    }

    //obsが破棄された後にUpdateが呼ばれた!
    sbj.Update();
}

repl.itのリンク先で、実行してみてください。
Observerインスタンスが破棄された後に、ハンドラが呼ばれてしまいましたね。運が良ければ、問題なく動作しますが、運が悪いと、Segmentation Faultになるでしょう。
謎の強制終了バグに頭を悩ませる未来が想像できます。

C++11からは、原則生ポインタを使うのは避けるべきです。スマートポインタを使いましょう。

ここでは、Observerのインスタンスで、shared_ptrで管理されているとします。そして、ハンドラは weak_ptr 経由で呼ぶようにします。

なぜshared_ptrでなくweak_ptrを使うかといと、shared_ptrを使うと、Observerインスタンスがハンドラ側から参照されつづけ、メモリリークになってしまうためです。

さて、weak_ptr経由でメソッドを呼ぶ関数を考えます。

void CallFromWeak(std::weak_ptr<HogeClass> w)
{
    std::shared_ptr<HogeClass> s = w.lock();
    if( s ) { //ポインタが有効か?
        s->HogeMethod();
    }
}

こうすれば、ポインタが無効の時は、HogeMethodが呼ばれません。
ではこのCallFromWeak関数を、ハンドラに登録すればいいんですね!

・・・それでもいいのですが、このCallFromWeakは、HogeClassのHogeMethod限定になっています。別のクラスや別のメソッドを登録したいと思ったら、また似たような CallFromWeak を作らねばなりません。

さてどうしたものか・・・汎用的なCallFromWeakが欲しい。
そうだ、テンプレートにしよう。

//メンバー関数型
template<typename T, typename... Args>
using FuncType = void (T::*)(Args...);

//weak_ptr経由で呼ぶ
template<typename T, typename... Args>
static void CallFromWeak( std::weak_ptr<T> weak, FuncType<T, Args...> func, Args... args ) {
    std::shared_ptr<T> shared = weak.lock();
    if( shared ) {
        std::mem_fun(func)( shared.get(), args... );
    }
}

これを使って、最初のNGプログラムを修正してみます。


.......省略

//ハンドラ呼ばれる側
class Observer : public std::enable_shared_from_this<Observer>
{
public:
    Observer() {
    }
    virtual ~Observer() {
        cout << "destructed" << endl;
    }
    void Setup( Subject& sbj ) {
        auto shared = shared_from_this();
        std::weak_ptr<Observer> weak = shared;

        //いろんなメソッドが登録できる!
        sbj.AddHandler(std::bind(&CallFromWeak<Observer>, weak, &Observer::OnNotify1));
        sbj.AddHandler(std::bind(&CallFromWeak<Observer,int>, weak, &Observer::OnNotify2, 1));
    }
    void OnNotify1() {
        cout << "notified" << endl;
    }
    void OnNotify2(int i) {
        cout << "notified " << i << endl;
    }
};

int main() {
    Subject sbj;
    {
        auto obs = std::make_shared<Observer>();
        obs->Setup(sbj);
        sbj.Update();
    }
    sbj.Update();    
}

これで、安全にハンドラを呼び出せますね!

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5