C++でイベント駆動設計を行う際、Signal/Slotは非常に強力なパターンです。QtやBoost、sigslotなど優れたライブラリが存在しますが、ライブラリの場合自分好みにカスタマイズすることは叶いません。
本記事では、Signal/Slotの仕組みを外部ライブラリを使用せずに自作していきたいと思います。
Signal/Slotとは
Signal/Slot(シグナル・スロット)とは、イベントが起きたら、決められた処理を呼び出す仕組みのことです。
イベントが起きたことを通知する方をSignalといい、Signalの通知を受けて処理を実行する方をSlotといいます。
std::function<T>に似ていますが、Signal/Slotは一つのSignalに対して複数のSlotを接続できる点で、1つの関数のみを呼び出すstd::function<T>とはまた違った使い方ができます。
想定環境
- C++11以上(
std::functionlambdaautovectorを使用できる環境)
コード例
最も簡単な構造
まずは、基本の構造を作ってみたいと思います。
//--------------------------------------------------------------
// @File Signal.hpp
// @Brief Signalクラスの定義
//--------------------------------------------------------------
#pragma once
#include <vector>
#include <functional>
//--------------------------------------------------------------
// @brief Signalクラス。複数のSlotを接続・通知できる。
//--------------------------------------------------------------
class Signal {
public:
using SlotType = std::function<void()>; // Slot の型定義、引数なし、戻り値なしのstd::function
//--------------------------------------------------------------
// Slotを接続する。
//! @param slot [in] 接続する関数オブジェクト
//--------------------------------------------------------------
void Connect(const SlotType& slot);
//--------------------------------------------------------------
// Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Emit() const;
private:
std::vector<SlotType> slots_; /// 接続された Slot のstd::vector
};
#include "Signal.hpp"
//--------------------------------------------------------------
//! @brief Slotを接続する。
//--------------------------------------------------------------
void Signal::Connect(const SlotType& slot) {
slots_.push_back(slot); // slots_に処理を追加
}
//--------------------------------------------------------------
//! @brief Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Signal::Emit() const {
for (const auto& slot : slots_) {
slot();
}
}
#include <iostream>
#include "signal.hpp"
//--------------------------------------------------------------
//! @brief おはようを出力するだけの関数
//--------------------------------------------------------------
void MorningGreet() {
std::cout << "Good Morning!\n";
}
int main() {
Signal signal;//シグナルオブジェクトを作成
// 関数ポインタを接続
signal.Connect(MorningGreet);
// こんばんはを出力するラムダ式を作成
auto evening_greet = []() {
std::cout << "Good Evening!\n";
};
// ラムダ式を接続
signal.Connect(evening_greet);
// Signalを発火
signal.Emit();
return 0;
}
Good Morning!
Good Evening!
接続した関数を一度の発火で呼び出せたようです。
接続
SignalクラスのConnect()メンバ関数で、引数と戻り値のない関数やラムダ式をSlotとして接続しています。
//--------------------------------------------------------------
//! @brief Slotを接続する。
//--------------------------------------------------------------
void Signal::Connect(const SlotType& slot) {
slots_.push_back(slot); // slots_に処理を追加
}
// 関数ポインタを接続
signal.Connect(MorningGreet);
// こんばんはを出力するラムダ式を作成
auto evening_greet = []() {
std::cout << "Good Evening!\n";
};
// ラムダ式を接続
signal.Connect(evening_greet);
発火
SignalクラスのEmit()メンバ関数を発火させる(呼ぶ)ことで、接続した関数を呼び出します。
//--------------------------------------------------------------
//! @brief Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Signal::Emit() const {
for (const auto& slot : slots_) {
slot();
}
}
// Signalを発火
signal.Emit();
優先度をつけた構造
このままのコードでは、発火時に接続された順に関数を呼び出します。
接続順に関わらず、優先度をつけることで、おはようをこんばんはの後に接続しても、おはようが先に処理されるようにしてみたいと思います。
//--------------------------------------------------------------
// @File Slot.hpp
// @Brief Slotクラスの定義
//--------------------------------------------------------------
#pragma once
#include <functional>
//--------------------------------------------------------------
// @brief Slotクラス。関数と優先度を保持する。
//--------------------------------------------------------------
class Slot {
public:
using Callback = std::function<void()>; // コールバック関数の型定義
//--------------------------------------------------------------
// 引数付きコンストラクタ
//! @param func [in] コールバック関数
//! @param priority [in] 優先度(小さいほど高優先度)
//--------------------------------------------------------------
Slot(Callback func, int priority = 0);
//--------------------------------------------------------------
// ()の演算子オーバーロード、Slotを呼び出す。
//--------------------------------------------------------------
void operator()() const;
//--------------------------------------------------------------
// 優先度を取得する。
//--------------------------------------------------------------
int GetPriority() const;
//--------------------------------------------------------------
// <の演算子オーバーロード優先度で比較(小さい値ほど高優先度)
//! @param other [in] 比較対象のSlot
//--------------------------------------------------------------
bool operator<(const Slot& other) const;
private:
Callback func_; //発火時に呼び出す関数オブジェクト
int priority_; //呼び出す優先度(小さいほど高順位)
};
#include "Slot.hpp"
//--------------------------------------------------------------
//! @brief 引数付きコンストラクタ
//--------------------------------------------------------------
Slot::Slot(Callback func, int priority)
: func_(std::move(func)), priority_(priority) {
}
//--------------------------------------------------------------
//! @brief ()の演算子オーバーロード、Slotを呼び出す。
//--------------------------------------------------------------
void Slot::operator()() const {
if (func_) func_();
}
//--------------------------------------------------------------
//! @brief 優先度を取得する。
//--------------------------------------------------------------
int Slot::GetPriority() const { return priority_; }
//--------------------------------------------------------------
//! @brief <の演算子オーバーロード優先度で比較(小さい値ほど高優先度)
//--------------------------------------------------------------
bool Slot::operator<(const Slot& other) const {
return priority_ < other.priority_;
}
//--------------------------------------------------------------
// @File Signal.hpp
// @Brief Signalクラスの定義
//--------------------------------------------------------------
#pragma once
#include <vector>
#include "Slot.hpp"
//--------------------------------------------------------------
// @brief Signalクラス。複数のSlotを接続・通知できる。
//--------------------------------------------------------------
class Signal {
public:
//--------------------------------------------------------------
// Slotを接続する。
//! @param slot [in] 接続する関数オブジェクト
//! @param priority_ [in] 優先度(小さいほど高優先度)
//--------------------------------------------------------------
void Connect(const Slot::Callback& func, int priority = 0);
//--------------------------------------------------------------
// Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Emit() const;
private:
std::vector<Slot> slots_; /// 接続された Slot のstd::vector
};
#include "Signal.hpp"
#include <algorithm>
//--------------------------------------------------------------
//! @brief Slotを接続する。
//--------------------------------------------------------------
void Signal::Connect(const Slot::Callback& func, int priority) {
slots_.emplace_back(func, priority); // slots_に処理を追加
}
//--------------------------------------------------------------
//! @brief Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Signal::Emit() const {
// 優先度の高い順にソートして呼び出し
std::vector<Slot> sorted = slots_;
std::sort(sorted.begin(), sorted.end());// Slotの<演算子オーバーロードを使用してソート
for (const auto& slot : sorted) {
slot();
}
}
#include <iostream>
#include "signal.hpp"
//--------------------------------------------------------------
//! @brief おはようを出力するだけの関数
//--------------------------------------------------------------
void MorningGreet() {
std::cout << "Good Morning!\n";
}
int main() {
Signal signal;//シグナルオブジェクトを作成
// こんばんはを出力するラムダ式を作成
auto evening_greet = []() {
std::cout << "Good Evening!\n";
};
// ラムダ式を接続
signal.Connect(evening_greet, 1);
// 関数ポインタを接続
signal.Connect(MorningGreet,0);
// Signal を発火
signal.Emit();
return 0;
}
Good Morning!
Good Evening!
接続順に関わらず、優先度順に処理が行われるようになりました。
優先度/Priority
上記のコードでは、Slotクラスを定義して、メンバ変数に優先度を持つことで、コールバックの優先順位を管理しています。
演算子オーバーロードで<を優先度順のbool値を返すようにすることで、SignalでSlotのソートを行えるようにしています。
//--------------------------------------------------------------
//! @brief <の演算子オーバーロード優先度で比較(小さい値ほど高優先度)
//--------------------------------------------------------------
bool Slot::operator<(const Slot& other) const {
return priority_ < other.priority_;
}
ソート順に関数を呼び出すことで、優先度の実装を実現しています。
//--------------------------------------------------------------
//! @brief Signalを発火し、すべてのSlotを呼び出す。
//--------------------------------------------------------------
void Signal::Emit() const {
// 優先度の高い順にソートして呼び出し
std::vector<Slot> sorted = slots_;
std::sort(sorted.begin(), sorted.end());// Slotの<演算子オーバーロードを使用してソート
for (const auto& slot : sorted) {
slot();
}
}
総括
-
Signal/Slotのパターンを使用することで、イベント駆動型の設計が可能になる。 -
Signal/Slotでは一度のイベント発火で複数の処理をコールバックできる。 - ソートアルゴリズムを活用することで、優先順位をつけて
Slotの管理を行うことができる。