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();
}
これで、安全にハンドラを呼び出せますね!