2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C++】RAIIで安心!軽量イベントバス【TsukinoEventBus】を作ってみた

2
Posted at

C++でイベント駆動の仕組みを実装しようとすると、意外と複雑になりがちです。
Boost.Signals2Qtのような強力なライブラリもありますが、初心者にとっては導入が難しく、「もっとシンプルにイベントを購読・発行できる仕組みが欲しい」と感じることが多いのではないでしょうか。

そこで、軽量で安全に使えるC++向けイベントバスライブラリTsukinoEventBus を作りました。
RAIIによる購読解除の自動化や、優先度付きイベント処理、継承対応など、 「初心者でも安心して使える API」を意識して設計しています。

本記事では、このライブラリの特徴と使い方を解説します。
さらに内部の仕組みも紹介するので、読み終わるころには 自分好みにカスタマイズできる力 が身についているはずです。

TsukinoEventBusライブラリ

前提知識

  • C++の基礎文法を理解している
  • EventBusについて知っている

上記の記事の内容を前提に進めるので、イベント駆動設計EventBusについてよくわからない方は先にこちらへ目を通しておくことを推奨します。

ライブラリの概要

TsukinoEventBus は、C++向けに設計された軽量イベントバスライブラリです。
「イベント駆動設計をもっとシンプルに、そして安全に」という思想のもと、初心者でも安心して使える APIを提供しています。

特徴

  • 軽量 & ヘッダオンリー
    • include/ ディレクトリをプロジェクトに追加するだけで利用可能。外部依存なし
  • RAII による安全な購読解除
    • SubscriptionHandle がスコープを抜けると自動で購読解除され、解除忘れによるバグを防止
  • 優先度付きイベント処理
    • 複数の購読者がいる場合、優先度を設定して処理順を制御可能
  • 継承対応
    • 親クラス購読者が子イベントを受け取れる柔軟な設計
  • シンプルな API
    • subscribepublish の直感的なインターフェースで、すぐに使い始められる

想定ユースケース

  • 小規模ゲームのイベント管理
  • GUIアプリケーションのイベント通知
  • 学習用途としてのイベント駆動設計の理解

他ライブラリとの違い

📊 他ライブラリとの比較

項目 TsukinoEventBus Boost.Signals2 Qt Signals/Slots
導入の容易さ ヘッダオンリー、include/ を追加するだけ Boost 全体の導入が必要、依存が大きい Qt フレームワーク全体が必要
対象規模 小規模〜中規模、学習用途に最適 中規模〜大規模、商用利用実績あり 大規模 GUI アプリやゲームに最適
購読解除 RAII による自動解除 (SubscriptionHandle) 手動解除が必要、RAII は標準でなし QObject のライフサイクルに依存
優先度制御 サポートあり(購読時に設定可能) 標準ではなし 標準ではなし
継承対応 親クラス購読者が子イベントを受け取れる 型安全だが継承イベントは直接対応なし QObject 継承ベースで対応可能
スレッドセーフティ 未対応(拡張が必要) スレッドセーフ設計あり スレッド間通信に対応(QueuedConnection)
依存関係 なし( C++標準のみ) Boost ライブラリに依存 Qt フレームワークに依存
学習用途 ◎ 初心者向け、シンプルで安心 △ 高機能だが初心者には難しい △ GUI 前提で学習用途には重い

使い方チュートリアル

このセクションでは、リポジトリのexampleディレクトリ内のチュートリアルについて、詳細なチュートリアルを行います。

最も基本的な使用例、イベントの購読と発行の方法

example1_basic.cpp
//--------------------------------------------------------------
//! @file	example1_basic.cpp
//! @brief  TsukinoEventBusの基本的な使用例、イベントの購読と発行の方法
//! @author 山﨑愛 ( Qiita:tsukino_   github:tsukino)
//--------------------------------------------------------------
#include "TsukinoEventBus/TsukinoEventBus.hpp"
#include <iostream>
//--------------------------------------------------------------
//! @class HelloEvent  
//! @brief サンプルイベントクラス
//--------------------------------------------------------------
class HelloEvent : public TsukinoEventBus::BaseEvent {
public:
	std::string message;    // メッセージ内容を保持するメンバ変数

	//--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param msg [in] メッセージ内容
	//--------------------------------------------------------------
	HelloEvent(const std::string& msg) : message(msg) {}
};

// エントリポイント
int main() {
	// イベントバスの作成
	TsukinoEventBus::EventBus bus;

	// HelloEvent用のコールバック関数を定義
	auto hello_event_callback = [](const HelloEvent& e) {
		std::cout << "HelloEvent Callback: " << e.message << std::endl;
		};

	// イベント購読(RAIIハンドルを保持)
	auto handle = bus.subscribe<HelloEvent>(
		hello_event_callback,   // コールバック関数
		1                       // 優先度
	);

	// イベント発行
	bus.publish(HelloEvent("Hello EventBus!"));
	// handle がスコープを抜けると自動的に購読解除される
	// プログラム終了
	return 0;
}
result
HelloEvent Callback: Hello EventBus!

イベントクラス

TsukinoEventBusでは、イベントを表すクラスを定義して利用します。
イベントクラスはBaseEventを継承して作成します。これにより、EventBusが型安全にイベントを扱えるようになります。
発火時のコールバックに渡したい値はこのクラスを通してアクセスすることが出来ます。

example1_basic.cpp
//--------------------------------------------------------------
//! @class HelloEvent  
//! @brief サンプルイベントクラス
//--------------------------------------------------------------
class HelloEvent : public TsukinoEventBus::BaseEvent {
public:
	std::string message;    // メッセージ内容を保持するメンバ変数

	//--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param msg [in] メッセージ内容
	//--------------------------------------------------------------
	HelloEvent(const std::string& msg) : message(msg) {}
};

subscribe/購読

EventBusクラスのsubscribe<>()を呼ぶことで、購読を行えます。
subscribe<>()については以下の通りです。

引数名 必須 説明
EventType テンプレート引数 (class T : BaseEvent) 購読対象となるイベントクラス。BaseEvent を継承している必要あり
callback std::function<void(const T&)> またはラムダ式 イベント発火時に呼び出される関数。イベントオブジェクトを引数として受け取る
priority int ✖ (省略可能) イベント処理の優先度。数値が大きいほど先に呼び出される

戻り値でRAIIのハンドルが渡されるので、購読を続ける間はハンドルをどこかで保持し続けてください。

RAII/Resource Acquisition Is Initializationとは「リソースの獲得と初期化を同時に行い、スコープを抜けると自動的に解放される」というC++の設計思想です。
TsukinoEventBusでは購読解除をRAIIによって自動化しており、解除忘れによるクラッシュを防ぐことができます。

example1_basic.cpp
// イベント購読(RAIIハンドルを保持)
auto handle = bus.subscribe<HelloEvent>(
	hello_event_callback,   // コールバック関数
	1                       // 優先度
);

publish/発行

EventBuspublish()を呼ぶことで、イベントの通知を行います。
引数にはイベントクラスを取り、subscribe<>()のテンプレートで指定した型に対応するコールバックを発火させます。

example1_basic.cpp
// イベント発行
bus.publish(HelloEvent("Hello EventBus!"));

複数購読者と優先度による呼び出し順序について

example2_priority.cpp
//--------------------------------------------------------------
//! @file	example2_priority.cpp
//! @brief  TsukinoEventBusの使用例、複数購読者と優先度による呼び出し順序
//! @author 山﨑愛 ( Qiita:tsukino_   github:tsukino)
//--------------------------------------------------------------
#include "TsukinoEventBus/TsukinoEventBus.hpp"
#include <iostream>
//--------------------------------------------------------------
//! @class PriorityEvent
//! @brief 優先度テスト用のイベントクラス
//--------------------------------------------------------------
class PriorityEvent : public TsukinoEventBus::BaseEvent {
public:
	std::string message;	// メッセージ内容を保持するメンバ変数
	
	//--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param msg [in] メッセージ内容
	//--------------------------------------------------------------
	explicit PriorityEvent(const std::string& msg) : message(msg) {}	
};

// エントリポイント
int main() {
	// イベントバスの作成
	TsukinoEventBus::EventBus bus;

	// 優先度の低い購読者 (priority = 1)
	auto low_handle = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[Low priority] " << e.message << std::endl;
		},
		1
	);

	// 優先度の高い購読者 (priority = 10)
	auto high_handle = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[High priority] " << e.message << std::endl;
		},
		10
	);

	// 同じ優先度の購読者 (priority = 10, 登録順で呼ばれる)
	auto high_handle2 = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[High priority, second registered] " << e.message << std::endl;
		},
		10
	);

	// イベント発行
	bus.publish(PriorityEvent("Priority test event"));
	// プログラム終了
	return 0;
}

result
[High priority] Priority test event
[High priority, second registered] Priority test event
[Low priority] Priority test event

priority/優先度

subscribe<>()の第二引数は優先度になっており、数値が高いほど優先度が高くなっております。
同じ数値の場合は登録順が早かった方を先に処理します。

example2_priority.cpp
// 優先度の低い購読者 (priority = 1)
	auto low_handle = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[Low priority] " << e.message << std::endl;
		},
		1
	);

	// 優先度の高い購読者 (priority = 10)
	auto high_handle = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[High priority] " << e.message << std::endl;
		},
		10
	);

	// 同じ優先度の購読者 (priority = 10, 登録順で呼ばれる)
	auto high_handle2 = bus.subscribe<PriorityEvent>(
		[](const PriorityEvent& e) {
			std::cout << "[High priority, second registered] " << e.message << std::endl;
		},
		10
	);

親クラス購読者は子クラスイベントを受け取れる

example3_inheritance.cpp
//--------------------------------------------------------------
//! @file   example3_inheritance.cpp
//! @brief	TsukinoEventBusの使用例、親クラス購読者が子クラスイベントを受け取れることを確認
//! @author 山﨑愛 ( Qiita:tsukino_   github:tsukino)
//--------------------------------------------------------------
#include "TsukinoEventBus/TsukinoEventBus.hpp"
#include <iostream>

//--------------------------------------------------------------
//! @class BaseGameEvent
//! @brief ゲーム関連イベントの基底クラス
//--------------------------------------------------------------
class BaseGameEvent : public TsukinoEventBus::BaseEvent {
public:
	//--------------------------------------------------------------
	//! @brief デストラクタ
	//--------------------------------------------------------------
	virtual ~BaseGameEvent() = default;
};

//--------------------------------------------------------------
//! @class PlayerJoinedEvent
//! @brief プレイヤー参加イベント
//--------------------------------------------------------------
class PlayerJoinedEvent : public BaseGameEvent {
private:
	std::string name_;  // プレイヤー名
public:
	//--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param name [in] 参加したプレイヤーの名前
	//--------------------------------------------------------------
	explicit PlayerJoinedEvent(const std::string& name)
		: name_(name) {
	}

	//--------------------------------------------------------------
	//! @brief 参加したプレイヤーの名前を取得
	//! @return プレイヤー名の参照
	//--------------------------------------------------------------
	const std::string& getName() const { return name_; }
};

//--------------------------------------------------------------
//! @class ScoreUpdatedEvent
//! @brief スコア更新イベント
//--------------------------------------------------------------
class ScoreUpdatedEvent : public BaseGameEvent {
private:
	int score_; // 新しいスコア
public:

	//--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param score [in] 新しいスコア値
	//--------------------------------------------------------------
	explicit ScoreUpdatedEvent(int score)
		: score_(score) {
	}

	//--------------------------------------------------------------
	//! @brief 新しいスコア値を取得
	//! @return スコア値
	//--------------------------------------------------------------
	int getScore() const { return score_; }
};

// エントリポイント
int main() {
	TsukinoEventBus::EventBus bus;

	// 親クラス購読者(BaseGameEventを購読)
	auto base_handle = bus.subscribe<BaseGameEvent>(
		[](const BaseGameEvent& e) {
			std::cout << "[BaseGameEvent subscriber] 子クラスイベントを受け取りました" << std::endl;
		},
		5
	);

	// 子クラス専用購読者(PlayerJoinedEvent)
	auto joined_handle = bus.subscribe<PlayerJoinedEvent>(
		[](const PlayerJoinedEvent& e) {
			std::cout << "[PlayerJoinedEvent subscriber] Player joined: " << e.getName() << std::endl;
		},
		10
	);

	// 子クラス専用購読者(ScoreUpdatedEvent)
	auto score_handle = bus.subscribe<ScoreUpdatedEvent>(
		[](const ScoreUpdatedEvent& e) {
			std::cout << "[ScoreUpdatedEvent subscriber] Score updated: " << e.getScore() << std::endl;
		},
		8
	);

	// イベント発行
	bus.publish(PlayerJoinedEvent("Alice"));
	bus.publish(ScoreUpdatedEvent(42));
	// プログラム終了
	return 0;
}

親クラスの購読

親クラス(本コードではBaseGameEvent)を購読した場合、子クラス(本コードではPlayerJoinedEventScoreUpdatedEvent)を引数としたイベントの発行(publish)時にも、イベントが発火する仕組みとなっています。

購読中のコールバックの書き換え

example4_update_callback.cpp
//--------------------------------------------------------------
//! @file   example4_update_callback.cpp
//! @brief  TsukinoEventBusの使用例、購読中のコールバックを動的に更新する方法
//! @author 山﨑愛 ( Qiita:tsukino_   github:tsukino)
//--------------------------------------------------------------
#include "TsukinoEventBus/TsukinoEventBus.hpp"
#include <iostream>

//--------------------------------------------------------------
//! @class UpdateEvent
//! @brief コールバック更新テスト用のイベントクラス
//--------------------------------------------------------------
class UpdateEvent : public TsukinoEventBus::BaseEvent {
private:
    std::string message_;  // メッセージ内容
public:
    //--------------------------------------------------------------
    //! @brief コンストラクタ
    //! @param msg [in] メッセージ内容
    //--------------------------------------------------------------
    explicit UpdateEvent(const std::string& msg) : message_(msg) {}

    //--------------------------------------------------------------
    //! @brief メッセージ内容を取得
    //! @return メッセージ文字列
    //--------------------------------------------------------------
    const std::string& getMessage() const { return message_; }
};

// エントリポイント
int main() {
	// イベントバスの作成
    TsukinoEventBus::EventBus bus;

    // 初期コールバックを登録
    auto handle = bus.subscribe<UpdateEvent>(
        [](const UpdateEvent& e) {
            std::cout << "[Initial callback] " << e.getMessage() << std::endl;
        },
        5
    );

    // イベント発行(初期コールバックが呼ばれる)
    bus.publish(UpdateEvent("First message"));

    // コールバックを更新
    handle.updateCallback<UpdateEvent>(
        [](const UpdateEvent& e) {
            std::cout << "[Updated callback] " << e.getMessage() << std::endl;
        }
    );

    // イベント発行(更新後のコールバックが呼ばれる)
    bus.publish(UpdateEvent("Second message"));

    // プログラム終了時に handle がスコープを抜けて自動解除される
    return 0;
}
result
[Initial callback] First message
[Updated callback] Second message

RAIIハンドル/SubscriptionHandleupdateCallback<>()を呼び出すことで、一度登録した関数を書き直すことが出来ます。
引数は以下の通りです。

引数名 必須 説明
EventType テンプレート引数 (class T : BaseEvent) 更新対象となるイベントクラス。BaseEvent を継承している必要あり
callback std::function<void(const T&)> またはラムダ式 新しく設定するコールバック関数。イベント発火時に呼び出される処理を差し替える
example4_update_callback.cpp
 // コールバックを更新
 handle.updateCallback<UpdateEvent>(
     [](const UpdateEvent& e) {
         std::cout << "[Updated callback] " << e.getMessage() << std::endl;
     }
 );

RAIIによる購読解除の自動管理を確認

example5_raii.cpp
//--------------------------------------------------------------
//! @file   example5_raii.cpp
//! @brief  TsukinoEventBusの使用例、RAIIによる購読解除の自動管理を確認
//! @author 山﨑愛 ( Qiita:tsukino_   github:tsukino)
//--------------------------------------------------------------
#include "TsukinoEventBus/TsukinoEventBus.hpp"
#include <iostream>

//--------------------------------------------------------------
//! @class ScopedEvent
//! @brief RAII動作確認用のイベントクラス
//--------------------------------------------------------------
class ScopedEvent : public TsukinoEventBus::BaseEvent {
private:
	std::string message_;   // メッセージ内容
public:
    //--------------------------------------------------------------
	//! @brief コンストラクタ
	//! @param msg [in] メッセージ内容
    //--------------------------------------------------------------
    explicit ScopedEvent(const std::string& msg) : message_(msg) {}

    //--------------------------------------------------------------
	//! @brief メッセージ内容を取得
	//! @return メッセージ文字列
    //--------------------------------------------------------------
    const std::string& getMessage() const { return message_; }
};

// エントリポイント
int main() {
	// イベントバスの作成
    TsukinoEventBus::EventBus bus;

    {
        // スコープ内で購読を登録
        auto scoped_handle = bus.subscribe<ScopedEvent>(
            [](const ScopedEvent& e) {
                std::cout << "[Scoped callback] " << e.getMessage() << std::endl;
            },
            5
        );

        // スコープ内でイベント発行 → コールバックが呼ばれる
        bus.publish(ScopedEvent("Inside scope"));
    } // scoped_handle がスコープを抜けると自動的に購読解除される

    // スコープ外でイベント発行 → すでに購読解除されているため呼ばれない
    bus.publish(ScopedEvent("Outside scope"));
	// プログラム終了
    return 0;
}
result
[Scoped callback] Inside scope

RAIIによる自動購読解除

RAIIハンドル/SubscriptionHandleがスコープを抜けた際には、デストラクタによって自動でイベントの購読が解除されます。

example5_raii.cpp
 {
     // スコープ内で購読を登録
     auto scoped_handle = bus.subscribe<ScopedEvent>(
         [](const ScopedEvent& e) {
             std::cout << "[Scoped callback] " << e.getMessage() << std::endl;
         },
         5
     );

     // スコープ内でイベント発行 → コールバックが呼ばれる
     bus.publish(ScopedEvent("Inside scope"));
 } // scoped_handle がスコープを抜けると自動的に購読解除される

内部の仕組み

ここでは TsukinoEventBus の内部設計について解説します。
利用者はAPIを直感的に使えますが、その裏側では以下のような仕組みで動いています。
記事内では抜粋して紹介するため、コード全体を見たい方はgithubをご参照ください。

全体の図解

EventBus の構造

TsukinoEventBus.hpp
//--------------------------------------------------------------
//! @class EventBus
//! @brief イベントバスシステムの本体
//! @note  優先度付き・型安全なイベントシステムを提供します
//--------------------------------------------------------------
class EventBus {
public:
	//コールバックの型エイリアス
	using Callback = std::function<void(const BaseEvent&)>;
public:
	//--------------------------------------------------------------
	//! @brief	   購読登録
	//! @tparam    T		 [in] イベントの型
	//! @param     callback [in] イベント発行時に呼び出されるコールバック関数
	//! @param     priority [in] コールバックの優先度 ( 数値が大きいほど優先度が高い )
	//! @return    SubscriptionHandle 購読解除用のRAIIハンドル
	//! @note     この関数内でソートを行います
	//! @note      同じ優先度の場合は登録順に呼び出されます
	//! @note      親クラスの型で購読登録することも可能、子クラスのイベントも受け取れます
	//--------------------------------------------------------------
	template<typename T>
	SubscriptionHandle subscribe(std::function<void(const T&)> callback, int priority);

	//--------------------------------------------------------------
	//! @brief 購読者のコールバックを更新する
	//! @param id			[in] 購読者ID
	//! @param new_callback [in] 新しいコールバック関数
	//! @return true: 更新成功 / false: 該当購読者なし
	//--------------------------------------------------------------
	bool updateCallback(size_t id, Callback new_callback);

	//--------------------------------------------------------------
	//! @brief 購読解除
	//! @param id [in] 購読者ID 
	//! @note RAIIハンドルのデストラクタから呼び出されます 
	//--------------------------------------------------------------
	void unsubscribe(size_t id);

	//--------------------------------------------------------------
	//! @brief   イベント発行
	//! @param   event [in] イベントオブジェクト
	//! @note    親クラス購読者は子クラスイベントも受け取ることができます。
	//! @note    購読者リストを走査し、dynamic_cast により適切な型だけ呼び出されます。
	//! @warning 該当する購読者が存在しない場合は処理は行われません。
	//--------------------------------------------------------------
	void publish(const BaseEvent& event);

private:
	//--------------------------------------------------------------
	//! @struct Subscriber
	//! @brief  購読者情報を格納する構造体
	//--------------------------------------------------------------
	struct Subscriber {
		Callback callback;	//!< コールバック関数
		int priority;		//!< 優先度
		size_t id;			//!< 購読者ID
	};
private:
	std::vector<Subscriber> subscribers_;	// 購読者リスト
	size_t nextId_ = 0;						// 次の購読者ID
};

//--------------------------------------------------------------
// EventBusのコンストラクタ・メンバ関数実装
//--------------------------------------------------------------

//--------------------------------------------------------------
//! @brief 購読登録
//--------------------------------------------------------------
template<typename T>
inline SubscriptionHandle EventBus::subscribe(std::function<void(const T&)> callback, int priority) {
	//ラッパー関数を作成
	auto wrapper = [callback](const BaseEvent& event) {
		// ダウンキャストを試みる
		if (auto derived = dynamic_cast<const T*>(&event)) {
			callback(*derived); // キャスト成功時のみ呼び出す 
		}
		};
	//購読者IDを発行
	size_t id = nextId_++;

	//購読者リストに追加
	subscribers_.emplace_back(wrapper, priority, id);
	// 優先度でソート
	std::stable_sort(subscribers_.begin(), subscribers_.end(),
		[](const Subscriber& lhs, const Subscriber& rhs) {
			return lhs.priority > rhs.priority;
		});
	//購読ハンドルを返す
	return SubscriptionHandle(*this, id);
}

//--------------------------------------------------------------
//! @brief 購読者のコールバックを更新する
//! @param id			[in] 購読者ID
//! @param new_callback [in] 新しいコールバック関数
//! @return true: 更新成功 / false: 該当購読者なし
//--------------------------------------------------------------
inline bool EventBus::updateCallback(size_t id, Callback new_callback) {
	for (auto& subscriber : subscribers_) {
		if (subscriber.id == id) {
			subscriber.callback = std::move(new_callback);
			return true; // 更新成功
		}
	}
	return false; // 該当購読者なし
}

//--------------------------------------------------------------
//! @brief 購読解除
//! @param id [in] 購読者ID 
//! @note RAIIハンドルのデストラクタから呼び出されます 
//--------------------------------------------------------------
inline void EventBus::unsubscribe(size_t id) {
	subscribers_.erase(std::remove_if(subscribers_.begin(), subscribers_.end(),
		[&](const Subscriber& s) { return s.id == id; }),
		subscribers_.end());
}

//--------------------------------------------------------------
//! @brief   イベント発行
//! @param   event [in] イベントオブジェクト
//! @note    親クラス購読者は子クラスイベントも受け取ることができます。
//! @note    購読者リストを走査し、dynamic_cast により適切な型だけ呼び出されます。
//! @warning 該当する購読者が存在しない場合は処理は行われません。
//--------------------------------------------------------------
inline void EventBus::publish(const BaseEvent& event) {
	//イベントの型情報を取得
	for (const auto& subscriber : subscribers_) {
		subscriber.callback(event); // dynamic_cast により適切な型だけ呼ばれる
	}
}

subscribe/購読

EventBus は登録したコールバックをラムダ式でラッパーし、購読者リストとして保持します。

ラッパー内でテンプレートの型を使ってダウンキャストを試みることで、親クラスで登録したコールバックを子クラスの発火によって呼び出すことが可能となっています。

この時に購読者ID(id)を発行することで、RAIIによる自動購読解除を可能にしています。

また、リスト追加の後に優先度でソートを行うことで、コールバックの優先度を実装しています。

TsukinoEventBus.hpp
//--------------------------------------------------------------
//! @brief 購読登録
//--------------------------------------------------------------
template<typename T>
inline SubscriptionHandle EventBus::subscribe(std::function<void(const T&)> callback, int priority) {
	//ラッパー関数を作成
	auto wrapper = [callback](const BaseEvent& event) {
		// ダウンキャストを試みる
		if (auto derived = dynamic_cast<const T*>(&event)) {
			callback(*derived); // キャスト成功時のみ呼び出す 
		}
		};
	//購読者IDを発行
	size_t id = nextId_++;

	//購読者リストに追加
	subscribers_.emplace_back(wrapper, priority, id);
	// 優先度でソート
	std::stable_sort(subscribers_.begin(), subscribers_.end(),
		[](const Subscriber& lhs, const Subscriber& rhs) {
			return lhs.priority > rhs.priority;
		});
	//購読ハンドルを返す
	return SubscriptionHandle(*this, id);
}

publish/発行

TsukinoEventBusでは、購読者リストをすべて走査し、対応する型のみがコールバックされる仕組みとなっています。

TsukinoEventBus.hpp
//--------------------------------------------------------------
//! @brief   イベント発行
//! @param   event [in] イベントオブジェクト
//! @note    親クラス購読者は子クラスイベントも受け取ることができます。
//! @note    購読者リストを走査し、dynamic_cast により適切な型だけ呼び出されます。
//! @warning 該当する購読者が存在しない場合は処理は行われません。
//--------------------------------------------------------------
inline void EventBus::publish(const BaseEvent& event) {
	//イベントの型情報を取得
	for (const auto& subscriber : subscribers_) {
		subscriber.callback(event); // dynamic_cast により適切な型だけ呼ばれる
	}
}

SubscriptionHandleとRAII

TsukinoEventBus.hpp
//--------------------------------------------------------------
//! @class SubscriptionHandle
//! @brief 購読解除をRAIIで管理するハンドル
//! @details
//! - EventBus::subscribe() から返されます
//! - ハンドルがスコープを抜けると自動的に購読解除されます 
//! - コピーは禁止、ムーブのみ可能です
//-------------------------------------------------------------- 
class SubscriptionHandle {
public:
	//--------------------------------------------------------------
	// コンストラクタ
	//! @param bus [in] 購読登録されたイベントバスの参照
	//! @param id  [in] 購読登録ID
	//--------------------------------------------------------------
	SubscriptionHandle(EventBus& bus, size_t id);

	//--------------------------------------------------------------
	// デストラクタ
	//! @note 自動的に購読解除を行います
	//--------------------------------------------------------------
	~SubscriptionHandle()noexcept;

	SubscriptionHandle(const SubscriptionHandle&) = delete;				// コピー禁止
	SubscriptionHandle& operator=(const SubscriptionHandle&) = delete;  // コピー禁止
	
	//--------------------------------------------------------------
	// ムーブコンストラクタ
	//! @param other [in] 移動元のSubscriptionHandle
	//! @note  ムーブ後、ムーブ元を無効化します
	//--------------------------------------------------------------
	SubscriptionHandle(SubscriptionHandle&& other)noexcept;

	//--------------------------------------------------------------
	// ムーブ代入
	//! @param other [in] 移動元のSubscriptionHandle
	//! @note  ムーブ後、ムーブ元を無効化します
	//--------------------------------------------------------------
	SubscriptionHandle& operator=(SubscriptionHandle&& other)noexcept;	// ムーブ代入

	//--------------------------------------------------------------
	// 型安全に購読中のコールバックを更新する
	//! @tparam T [in] イベント型
	//! @param newCallback [in] 新しいコールバック関数
	//! @return true: 更新成功 / false: 解除済み or 該当なし
	//--------------------------------------------------------------
	template<typename T>
	bool updateCallback(std::function<void(const T&)> newCallback);

	//--------------------------------------------------------------
	// 明示的に購読解除を行う関数
	//! @return true: 解除成功 / false: すでに解除済み
	//! @note  解除済みの場合は何もしません 
	//--------------------------------------------------------------
	bool release();
private:
	EventBus* bus_;	//!< 購読登録されたイベントバスの参照(解除でnullptrにする)
	size_t id_;		//!< 購読登録ID
	bool released_; //!< 解除済みフラグ
};

//--------------------------------------------------------------
// SubscriptionHandleのコンストラクタ・デストラクタ・メンバ関数実装
//--------------------------------------------------------------

//--------------------------------------------------------------
//! @brief コンストラクタ
//--------------------------------------------------------------
inline SubscriptionHandle::SubscriptionHandle(EventBus& bus, size_t id)
	: bus_(&bus), id_(id), released_(false) {
}

//--------------------------------------------------------------
//! @brief デストラクタ
//--------------------------------------------------------------
inline SubscriptionHandle::~SubscriptionHandle()noexcept {
	// 自動的に購読解除を行う(二重解放を予防)
	release();
}

//--------------------------------------------------------------
//! @brief ムーブコンストラクタ
//--------------------------------------------------------------
inline SubscriptionHandle::SubscriptionHandle(SubscriptionHandle&& other) noexcept
	: bus_(other.bus_), id_(other.id_), released_(other.released_) {
	other.bus_ = nullptr;		// ムーブ元を無効化
	other.released_ = true;		// ムーブ元を解除済みに設定
}

//--------------------------------------------------------------
//! @brief ムーブ代入
//--------------------------------------------------------------
inline SubscriptionHandle& SubscriptionHandle::operator=(SubscriptionHandle&& other) noexcept {
	if (this != &other) {
		// 既存の購読を解除
		release();
		// ムーブ元からデータを移動
		bus_ = other.bus_;
		id_ = other.id_;
		released_ = other.released_;
		// ムーブ元を無効化
		other.bus_ = nullptr;
		other.released_ = true;
	}
	return *this;
}

//--------------------------------------------------------------
//! @brief 型安全に購読中のコールバックを更新する
//--------------------------------------------------------------
template<typename T>
inline bool SubscriptionHandle::updateCallback(std::function<void(const T&)> newCallback) {
	if (released_ || !bus_) {
		return false; // 解除済みまたは無効なバス
	}
	// 型安全なラッパー関数を作成
	auto wrapper = [newCallback](const BaseEvent& event) {
		// ダウンキャストを試みる
		if (auto derived = dynamic_cast<const T*>(&event)) {
			newCallback(*derived); // キャスト成功時のみ呼び出す 
		}
		};
	// イベントバスに更新を依頼
	return bus_->updateCallback(id_, wrapper);
}

//--------------------------------------------------------------
//! @brief 明示的に購読解除を行う関数
//--------------------------------------------------------------
inline bool SubscriptionHandle::release() {
	// まだ解除されていなければ解除を行う
	if (!released_ && bus_) {
		bus_->unsubscribe(id_);	// 購読解除
		released_ = true;		// 解除済みフラグを立てる
		bus_ = nullptr;			// bus_をnullptrにして再度解除されないようにする
		return true;			// 解除成功
	}
	return false;				// 既に解除済み
}

自動購読解除

subscribe<>() の戻り値であるSubscriptionHandle は購読情報への参照を持ち、デストラクタで自動解除を行います。
これにより「解除忘れ」によるクラッシュを防止できます。

TsukinoEventBus.hpp
//--------------------------------------------------------------
//! @brief デストラクタ
//--------------------------------------------------------------
inline SubscriptionHandle::~SubscriptionHandle()noexcept {
	// 自動的に購読解除を行う(二重解放を予防)
	release();
}

// 中略

//--------------------------------------------------------------
//! @brief 明示的に購読解除を行う関数
//--------------------------------------------------------------
inline bool SubscriptionHandle::release() {
	// まだ解除されていなければ解除を行う
	if (!released_ && bus_) {
		bus_->unsubscribe(id_);	// 購読解除
		released_ = true;		// 解除済みフラグを立てる
		bus_ = nullptr;			// bus_をnullptrにして再度解除されないようにする
		return true;			// 解除成功
	}
	return false;				// 既に解除済み
}

総括

本記事では TsukinoEventBus の特徴と使い方、そして内部の仕組みを解説しました。
イベント駆動設計は一見複雑に見えますが、以下のようなポイントを押さえることで初心者でも安心して利用できます。

  • シンプルな API
    subscribepublish の直感的なインターフェースで、すぐにイベント駆動設計を体験できる
  • RAII による安全性
    SubscriptionHandle がスコープを抜けると自動で購読解除され、解除忘れによるバグを防止
  • 優先度制御と継承対応
    複数購読者の呼び出し順序や、親クラス購読者が子イベントを受け取れる柔軟性を提供
  • 内部設計の理解
    購読者リスト、dynamic_cast による型判定、優先度付きソートなどの仕組みを把握することで、自分好みにカスタマイズ可能

TsukinoEventBus は「初心者でも安心してイベント駆動設計を使える・学べる」ことを目指して設計しました。
ぜひGitHubリポジトリの方もチェックして、スターやフィードバックをいただけると嬉しいです。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?