C++
C++11

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

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();
}

https://repl.it/@mas_yo/bind-weak-pointer-1

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();    
}

https://repl.it/@mas_yo/bind-weak-pointer

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