Sigslot は、C++ 用のシグナル スロットのヘッダーのみのスレッド セーフな実装です。
主な目標は、Boost.Signals2 を置き換えることでした。
通常の機能とは別に、以下の機能を提供します。
Sigslot は単体テストが行われており、Boost Signals2 を置き換えるのに十分な信頼性と安定性を備えている必要があります。
テストは、アドレス、スレッド、および未定義の動作サニタイザーの下で問題なく実行されます。
多くの実装では信号の戻り値の型が許可されていますが、Sigslot では信号の戻り値の型を使用できないため許可されていません。違うと確信できれば、後で考えが変わるかもしれません。
コンパイルやインストールは必要なく、 sigslot/signal.hpp
をインクルードして使用するだけです。現在、Sigslot は C++14 準拠のコンパイラに依存していますが、必要に応じて C++11 に改良される可能性があります。 GNU Linux 上の Clang 4.0 および GCC 5.0+ コンパイラ、MSVC 2017 以降、Windows 上の Clang-cl および MinGW で動作することが知られています。
ただし、MSVC および Clang-Cl コンパイラを使用する Windows では、例外的な状況で/OPT:NOICF
リンカー フラグが必要になる可能性があるという潜在的な問題に注意してください。説明については、「実装の詳細」の章を参照してください。
CMake リスト ファイルは、インストール目的と CMake インポート モジュールの生成のために提供されています。これは推奨されるインストール方法です。 Pal::Sigslot
インポートされたターゲットが利用可能で、必要なリンカー フラグがすでに適用されています。これは、アダプターの単体テストでオプションで Qt5 と Boost に依存する例とテストにも必要です。
# Using Sigslot from cmake
find_package (PalSigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
構成オプションSIGSLOT_REDUCE_COMPILE_TIME
は構成時に使用できます。アクティブにすると、 std::make_shared
の呼び出しによって生じる大量のテンプレートのインスタンス化を回避して、コードの肥大化を削減しようとします。このオプションはデフォルトではオフですが、コードの効率性がわずかに劣る場合でも、コード サイズとコンパイル時間を優先したい場合は、アクティブにすることができます。
インストールは、ルート ディレクトリから次の手順を使用して実行できます。
mkdir build && cd build
cmake .. -DSIGSLOT_REDUCE_COMPILE_TIME=ON -DCMAKE_INSTALL_PREFIX= ~ /local
cmake --build . --target install
# If you want to compile examples:
cmake --build . --target sigslot-examples
# And compile/execute unit tests:
cmake --build . --target sigslot-tests
Pal::Sigslot
FetchContent メソッドを使用して統合することもできます。
include (FetchContent)
FetchContent_Declare(
sigslot
GIT_REPOSITORY https://github.com/palacaze/sigslot
GIT_TAG 19a6f0f5ea11fc121fe67f81fd5e491f2d7a4637 # v1.2.0
)
FetchContent_MakeAvailable(sigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
Sigslot は、UI フレームワークで一般的なシグナル スロット構造を実装しており、オブザーバー パターンやイベントベースのプログラミングを簡単に使用できるようにします。ライブラリのメイン エントリ ポイントは、 sigslot::signal<T...>
クラス テンプレートです。
シグナルは、型指定された通知を発行できるオブジェクトであり、実際にはシグナル クラス テンプレート パラメーターの後にパラメーター化された値を発行し、シグナルの発行が発生するたびに指定された値で実行される互換性のある引数の型の通知ハンドラー (呼び出し可能オブジェクト) を任意の数だけ登録できます。信号スロット用語では、これをスロットを信号に接続すると呼びます。「スロット」は呼び出し可能なインスタンスを表し、「接続」は信号からスロットへの概念的なリンクと考えることができます。
以下に示すすべてのスニペットは、example サブディレクトリでコンパイル可能なソース コード形式で入手できます。
これは、ライブラリの最も基本的な機能を示す最初の例です。
最初にパラメーターのないシグナルsig
宣言し、次にいくつかのスロットの接続を進め、最後に、事前に接続されている呼び出し可能なすべてのスロットの呼び出しをトリガーするシグナルを発行します。ライブラリがさまざまな形式の呼び出し可能ファイルをどのように処理するかに注目してください。
# include < sigslot/signal.hpp >
# include < iostream >
void f () { std::cout << " free function n " ; }
struct s {
void m () { std::cout << " member function n " ; }
static void sm () { std::cout << " static member function n " ; }
};
struct o {
void operator ()() { std::cout << " function object n " ; }
};
int main () {
s d;
auto lambda = []() { std::cout << " lambda n " ; };
auto gen_lambda = []( auto && ... a ) { std::cout << " generic lambda n " ; };
// declare a signal instance with no arguments
sigslot:: signal <> sig;
// connect slots
sig. connect (f);
sig. connect (&s::m, &d);
sig. connect (&s::sm);
sig. connect ( o ());
sig. connect (lambda);
sig. connect (gen_lambda);
// a free connect() function is also available
sigslot::connect (sig, f);
// emit a signal
sig ();
}
デフォルトでは、シグナルを発行するときのスロット呼び出し順序は指定されていません。常に同じであることに依存しないでください。後で説明するスロット グループを使用して、特定の呼び出し順序を制限することができます。
最初の例は単純ですが、あまり役に立たなかったので、代わりに値を発行する信号に移りましょう。シグナルは、以下に示すように、任意の数の引数を発行できます。
# include < sigslot/signal.hpp >
# include < iostream >
# include < string >
struct foo {
// Notice how we accept a double as first argument here.
// This is fine because float is convertible to double.
// 's' is a reference and can thus be modified.
void bar ( double d, int i, bool b, std::string &s) {
s = b ? std::to_string (i) : std::to_string (d);
}
};
// Function objects can cope with default arguments and overloading.
// It does not work with static and member functions.
struct obj {
void operator ()( float , int , bool , std::string &, int = 0 ) {
std::cout << " I was here n " ;
}
void operator ()() {}
};
int main () {
// declare a signal with float, int, bool and string& arguments
sigslot:: signal < float , int , bool , std::string&> sig;
// a generic lambda that prints its arguments to stdout
auto printer = [] ( auto a, auto && ... args ) {
std::cout << a;
( void )std::initializer_list< int >{
(( void )(std::cout << " " << args), 1 )...
};
std::cout << " n " ;
};
// connect the slots
foo ff;
sig. connect (printer);
sig. connect (&foo::bar, &ff);
sig. connect ( obj ());
float f = 1 . f ;
short i = 2 ; // convertible to int
std::string s = " 0 " ;
// emit a signal
sig (f, i, false , s);
sig (f, i, true , s);
}
示されているように、スロットの引数の型は信号テンプレートのパラメーターと厳密に同一である必要はなく、変換元であれば問題ありません。 printer
ジェネリック ラムダで示されているように、ジェネリック引数も問題ありません (関数テンプレートとして記述することもできます)。
現時点では、呼び出し可能な処理に関して考えられる制限が 2 つあります。それは、デフォルト引数と関数のオーバーロードです。関数オブジェクトの場合はどちらも正しく動作しますが、静的関数やメンバー関数ではコンパイルに失敗します。理由は異なりますが関連しています。
次のコード部分を考えてみましょう。
struct foo {
void bar ( double d);
void bar ();
};
&foo::bar
何を参照する必要がありますか?オーバーロードに従って、メンバー関数に対するこのポインターは一意のシンボルにマップされないため、コンパイラーは正しいシンボルを選択できません。正しいシンボルを解決する 1 つの方法は、関数ポインターを正しい関数型に明示的にキャストすることです。以下は、より軽量な構文用の小さなヘルパー ツールを使用して、まさにそれを行う例です (実際、これはおそらくすぐにライブラリに追加されるでしょう)。
# include < sigslot/signal.hpp >
template < typename ... Args, typename C>
constexpr auto overload ( void (C::*ptr)(Args...)) {
return ptr;
}
template < typename ... Args>
constexpr auto overload ( void (*ptr)(Args...)) {
return ptr;
}
struct obj {
void operator ()( int ) const {}
void operator ()() {}
};
struct foo {
void bar ( int ) {}
void bar () {}
static void baz ( int ) {}
static void baz () {}
};
void moo ( int ) {}
void moo () {}
int main () {
sigslot:: signal < int > sig;
// connect the slots, casting to the right overload if necessary
foo ff;
sig. connect (overload< int >(&foo::bar), &ff);
sig. connect (overload< int >(&foo::baz));
sig. connect (overload< int >(&moo));
sig. connect ( obj ());
sig ( 0 );
return 0 ;
}
デフォルトの引数は関数の型シグネチャの一部ではなく、再定義できるため、扱うのが非常に困難です。スロットを信号に接続するとき、ライブラリは、指定された呼び出し可能オブジェクトを信号の引数の型で呼び出すことができるかどうかを判断しますが、この時点では、デフォルトの関数引数の存在が不明であるため、引数の数が一致しない可能性があります。
このユースケースの簡単な回避策は、バインド アダプターを作成することです。実際、次のように非常に汎用的なものにすることもできます。
# include < sigslot/signal.hpp >
# define ADAPT ( func )
[=]( auto && ...a) { (func)(std::forward< decltype (a)>(a)...); }
void foo ( int &i, int b = 1 ) {
i += b;
}
int main () {
int i = 0 ;
// fine, all the arguments are handled
sigslot:: signal < int &, int > sig1;
sig1. connect (foo);
sig1 (i, 2 );
// must wrap in an adapter
i = 0 ;
sigslot:: signal < int &> sig2;
sig2. connect ( ADAPT (foo));
sig2 (i);
return 0 ;
}
これまで明らかにされていなかったのは、 signal::connect()
実際に、シグナルスロット接続の動作と有効期間を管理するために使用できるsigslot::connection
オブジェクトを返すということです。 sigslot::connection
進行中の信号スロット接続との対話を可能にし、次の機能を公開する軽量オブジェクト (基本的にstd::weak_ptr
) です。
signal::connect()
によって作成された接続の破棄。 sigslot::connection
接続をスコープに結び付けません。これは RAII オブジェクトではないため、コピーできる理由が説明されています。ただし、スコープ外に出ると接続を破棄するsigslot::scoped_connection
に暗黙的に変換できます。
これらの機能の一部を示す例を次に示します。
# include < sigslot/signal.hpp >
# include < string >
int i = 0 ;
void f () { i += 1 ; }
int main () {
sigslot:: signal <> sig;
// keep a sigslot::connection object
auto c1 = sig. connect (f);
// disconnection
sig (); // i == 1
c1. disconnect ();
sig (); // i == 1
// scope based disconnection
{
sigslot::scoped_connection sc = sig. connect (f);
sig (); // i == 2
}
sig (); // i == 2;
// connection blocking
auto c2 = sig. connect (f);
sig (); // i == 3
c2. block ();
sig (); // i == 3
c2. unblock ();
sig (); // i == 4
}
Sigslot は、追加のsigslot::connection
参照を最初の引数として持つ拡張スロット署名をサポートしており、これによりスロット内からの接続管理が可能になります。この拡張署名には、 connect_extended()
メソッドを使用してアクセスできます。
# include < sigslot/signal.hpp >
int main () {
int i = 0 ;
sigslot:: signal <> sig;
// extended connection
auto f = []( auto &con) {
i += 1 ; // do work
con. disconnect (); // then disconnects
};
sig. connect_extended (f);
sig (); // i == 1
sig (); // i == 1 because f was disconnected
}
ユーザーはスロットの有効期間が信号の有効期間を超えていることを確認する必要がありますが、複雑なソフトウェアではこれが面倒になる可能性があります。このタスクを簡素化するために、Sigslot は、存続期間を追跡できるスロット オブジェクトを自動的に切断できます。これを行うには、スロットが何らかの形式の弱いポインタに変換可能である必要があります。
std::shared_ptr
およびstd::weak_ptr
そのままサポートされており、 boost::shared_ptr
、 boost::weak_ptr
、および Qt QSharedPointer
、 QWeakPointer
、およびQObject
から派生するクラスをサポートするアダプターが提供されています。
to_weak()
アダプター関数を宣言することで、他の追跡可能なオブジェクトを追加できます。
# include < sigslot/signal.hpp >
# include < sigslot/adapter/qt.hpp >
int sum = 0 ;
struct s {
void f ( int i) { sum += i; }
};
class MyObject : public QObject {
Q_OBJECT
public:
void add ( int i) const { sum += i; }
};
int main () {
sum = 0 ;
signal < int > sig;
// track lifetime of object and also connect to a member function
auto p = std::make_shared<s>();
sig. connect (&s::f, p);
sig ( 1 ); // sum == 1
p. reset ();
sig ( 1 ); // sum == 1
// track an unrelated object lifetime
struct dummy ;
auto l = [&]( int i) { sum += i; };
auto d = std::make_shared<dummy>();
sig. connect (l, d);
sig ( 1 ); // sum == 2
d. reset ();
sig ( 1 ); // sum == 2
// track a QObject
{
MyObject o;
sig. connect (&MyObject::add, &o);
sig ( 1 ); // sum == 3
}
sig ( 1 ); // sum == 3
}
メンバー関数スロット上のポインターの自動切断を確実にするもう 1 つの方法は、 sigslot::observer
またはsigslot::observer_st
から明示的に継承することです。後者とは対照的に、前者はスレッドセーフです。
以下に使用例を示します。
# include < sigslot/signal.hpp >
int sum = 0 ;
struct s : sigslot::observer_st {
void f ( int i) { sum += i; }
};
struct s_mt : sigslot::observer {
~s_mt () {
// Needed to ensure proper disconnection prior to object destruction
// in multithreaded contexts.
this -> disconnect_all ();
}
void f ( int i) { sum += i; }
};
int main () {
sum = 0 ;
signal < int > sig;
{
// Lifetime of object instance p is tracked
s p;
s_mt pm;
sig. connect (&s::f, &p);
sig. connect (&s_mt::f, &pm);
sig ( 1 ); // sum == 2
}
// The slots got disconnected at instance destruction
sig ( 1 ); // sum == 2
}
この侵入的なアプローチを使用するオブジェクトは、任意の数の無関係な信号に接続される可能性があります。
適切な関数シグネチャ、オブジェクト ポインター、またはトラッカーを提供することによるスロット切断のサポートがバージョン 1.2.0 で導入されました。
signal::disconnect()
を使用すると、任意の数のスロットを切断できます。このメソッドは、切断基準を指定する 4 つのオーバーロードを提案します。
ラムダの切断は、その一意性により、変数にバインドされたラムダでのみ可能です。
現在、2 番目のオーバーロードでは、メンバー関数、関数オブジェクト、ラムダへのポインターから切断するために RTTI が必要です。この制限は、無料の静的メンバー関数には適用されません。その理由は、C++ では、自由な静的メンバー関数へのポインターとは対照的に、無関係な型のメンバー関数へのポインターは比較できないという事実に由来します。たとえば、異なるクラスの仮想メソッドのメンバー関数へのポインタは、同じアドレスを持つことができます (メソッドのオフセットを vtable に格納するようなものです)。
ただし、Sigslot は RTTI を無効にしてコンパイルすることができ、問題がある場合にはオーバーロードが無効になります。
この機能はサイド ノードとして、扱いが難しく間違いやすいため、当初の予想よりも多くのコードが追加されたことは確かです。正確性を念頭に置いて慎重に設計されており、実際に使用しない限り隠れたコストはかかりません。
この機能を示す例を次に示します。
# include < sigslot/signal.hpp >
# include < string >
static int i = 0 ;
void f1 () { i += 1 ; }
void f2 () { i += 1 ; }
struct s {
void m1 () { i += 1 ; }
void m2 () { i += 1 ; }
void m3 () { i += 1 ; }
};
struct o {
void operator ()() { i += 1 ; }
};
int main () {
sigslot:: signal <> sig;
s s1;
auto s2 = std::make_shared<s>();
auto lbd = [&] { i += 1 ; };
sig. connect (f1); // #1
sig. connect (f2); // #2
sig. connect (&s::m1, &s1); // #3
sig. connect (&s::m2, &s1); // #4
sig. connect (&s::m3, &s1); // #5
sig. connect (&s::m1, s2); // #6
sig. connect (&s::m2, s2); // #7
sig. connect (o{}); // #8
sig. connect (lbd); // #9
sig (); // i == 9
sig. disconnect (f2); // #2 is removed
sig. disconnect (&s::m1); // #3 and #6 are removed
sig. disconnect (o{}); // #8 and is removed
// sig.disconnect(&o::operator()); // same as the above, more efficient
sig. disconnect (lbd); // #9 and is removed
sig. disconnect (s2); // #7 is removed
sig. disconnect (&s::m3, &s1); // #5 is removed, not #4
sig (); // i == 11
sig. disconnect_all (); // remove all remaining slots
return 0 ;
}
バージョン 1.2.0 以降、スロットの相対的な呼び出し順序を制御するために、スロットにグループ ID を割り当てることができます。
同じグループ内のスロットの呼び出し順序は指定されていないため、信頼すべきではありませんが、スロット グループはグループ ID の昇順で呼び出されます。スロットのグループ ID が設定されていない場合、そのスロットはグループ 0 に割り当てられます。グループ ID には、符号付き 32 ビット整数の範囲内の任意の値を指定できます。
# include < sigslot/signal.hpp >
# include < cstdio >
# include < limits >
int main () {
sigslot:: signal <> sig;
// simply assigning a group id as last argument to connect
sig. connect ([] { std::puts ( " Second " ); }, 1 );
sig. connect ([] { std::puts ( " Last " ); }, std::numeric_limits<sigslot::group_id>:: max ());
sig. connect ([] { std::puts ( " First " ); }, - 10 );
sig ();
return 0 ;
}
独立したsigslot::connect()
関数を使用すると、互換性のある引数を使用して信号を別の信号に接続できます。
# include < sigslot/signal.hpp >
# include < iostream >
int main () {
sigslot:: signal < int > sig1;
sigslot:: signal < double > sig2;
sigslot::connect (sig1, sig2);
sigslot::connect (sig2, [] ( double d) { std::cout << " got " << d << std::endl; });
sig ( 1 );
return 0 ;
}
スレッドの安全性は単体テストされています。特に、クロスシグナル エミッションと再帰的エミッションは、複数スレッドのシナリオで正常に動作します。
sigslot::signal
は、より一般的なsigslot::signal_base
テンプレート クラスの typedef であり、その最初のテンプレート引数は Lockable 型である必要があります。このタイプは、クラスのロック ポリシーを決定します。
Sigslot は 2 つの typedef を提供します。
sigslot::signal
複数のスレッドから使用でき、std::mutex をロック可能として使用します。特に、接続、切断、発行、およびスロットの実行はスレッド セーフです。再帰的な信号発信でも安全です。sigslot::signal_st
非スレッドセーフな代替手段であり、安全性と引き換えにわずかに高速な動作を実現します。 C++ では、関数ポインターの比較は悪夢です。以下は、ショーケースとしてさまざまなケースのサイズとアドレスを示した表です。
void fun () {}
struct b1 {
virtual ~b1 () = default ;
static void sm () {}
void m () {}
virtual void vm () {}
};
struct b2 {
virtual ~b2 () = default ;
static void sm () {}
void m () {}
virtual void vm () {}
};
struct c {
virtual ~c () = default ;
virtual void w () {}
};
struct d : b1 {
static void sm () {}
void m () {}
void vm () override {}
};
struct e : b1, c {
static void sm () {}
void m () {}
void vm () override {}
};
シンボル | GCC 9 Linux 64 サイズの | GCC 9 Linux 64 住所 | MSVC 16.6 32 サイズの | MSVC 16.6 32 住所 | GCC 8 ミング 32 サイズの | GCC 8 ミング 32 住所 | クランCL9 32 サイズの | クランCL9 32 住所 |
---|---|---|---|---|---|---|---|---|
楽しい | 8 | 0x802340 | 4 | 0x1311A6 | 4 | 0xF41540 | 4 | 0x0010AE |
&b1::sm | 8 | 0xE03140 | 4 | 0x7612A5 | 4 | 0x308D40 | 4 | 0x0010AE |
&b1::m | 16 | 0xF03240 | 4 | 0x1514A5 | 8 | 0x248D40 | 4 | 0x0010AE |
&b1::vm | 16 | 0x11 | 4 | 0x9F11A5 | 8 | 0x09 | 4 | 0x8023AE |
&b2::sm | 8 | 0x003340 | 4 | 0xA515A5 | 4 | 0x408D40 | 4 | 0x0010AE |
&b2::m | 16 | 0x103440 | 4 | 0xEB10A5 | 8 | 0x348D40 | 4 | 0x0010AE |
&b2::vm | 16 | 0x11 | 4 | 0x6A14A5 | 8 | 0x09 | 4 | 0x8023AE |
&d::sm | 8 | 0x203440 | 4 | 0x2612A5 | 4 | 0x108D40 | 4 | 0x0010AE |
&d::m | 16 | 0x303540 | 4 | 0x9D13A5 | 8 | 0x048D40 | 4 | 0x0010AE |
&d::vm | 16 | 0x11 | 4 | 0x4412A5 | 8 | 0x09 | 4 | 0x8023AE |
&e::sm | 8 | 0x403540 | 4 | 0xF911A5 | 4 | 0x208D40 | 4 | 0x0010AE |
&e::m | 16 | 0x503640 | 8 | 0x8111A5 | 8 | 0x148D40 | 8 | 0x0010AE |
&e::vm | 16 | 0x11 | 8 | 0xA911A5 | 8 | 0x09 | 8 | 0x8023AE |
リリース モードの MSVC と Clang-cl は、同じ定義を持つ関数をマージすることで最適化します。これは/OPT:NOICF
リンカー オプションを使用して無効にできる動作です。 Sigslot のテストと例は、この動作を引き起こす多くの同一の呼び出し可能ファイルに依存しているため、影響を受けるコンパイラでこの特定の最適化が無効になります。
バージョン 7.4 より前の GCC で汎用ラムダを使用すると、バグ #68071 が発生する可能性があります。