Sigslot 是 C++ 訊號槽的純頭檔、執行緒安全實作。
主要目標是取代 Boost.Signals2。
除了通常的功能外,它還提供
Sigslot 經過單元測試,應該要夠可靠且穩定,可以取代 Boost Signals2。
測試在地址、執行緒和未定義行為清理器下乾淨地運行。
許多實作允許訊號傳回類型,但 Sigslot 不允許,因為我對它們沒有用處。如果我能確信不這樣的話,我以後可能會改變主意。
無需編譯或安裝,只需包含sigslot/signal.hpp
並使用即可。 Sigslot 目前依賴 C++14 相容編譯器,但如果需要,它可能會改裝為 C++11。眾所周知,它可以在 GNU Linux、MSVC 2017 及更高版本上與 Clang 4.0 和 GCC 5.0+ 編譯器配合使用,在 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...>
類別範本。
訊號是一個可以發出類型化通知的對象,實際上是在信號類模板參數之後參數化的值,並註冊任意數量的兼容參數類型的通知處理程序(可調用對象),以便在信號發射發生時使用提供的值執行。在訊號槽術語中,這稱為將槽連接到訊號,其中「槽」表示可呼叫實例,而「連接」可以被認為是從訊號到槽的概念連結。
下面提供的所有程式碼片段都可以在範例子目錄中以可編譯原始程式碼的形式提供。
這是第一個範例,展示了該程式庫最基本的功能。
我們首先聲明一個無參數訊號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
通用 lambda 所示(也可以編寫為函數模板)。
現在,我可以想到關於可呼叫處理的兩個限制:預設參數和函數重載。兩者在函數物件的情況下都可以正常工作,但由於不同但相關的原因而無法使用靜態函數和成員函數進行編譯。
考慮下面的程式碼:
struct foo {
void bar ( double d);
void bar ();
};
&foo::bar
該指什麼?根據重載,成員函數上的指標不會對應到唯一的符號,因此編譯器將無法選擇正確的符號。解析正確符號的一種方法是將函數指標明確轉換為正確的函數類型。這是一個範例,它使用一個小幫助工具來實現更輕的語法(事實上,我可能很快就會將其添加到庫中)。
# 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
}
確保成員函式槽上的指標自動斷開連線的另一種方法是明確繼承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 個重載來指定斷開標準:
由於 lambda 的唯一性,只有綁定到變數的 lambda 才可能斷開 lambda。
第二個重載目前需要 RTTI 來斷開與成員函數、函數物件和 lambda 的指標的連接。此限制不適用於自由和靜態成員函數。原因源自於這樣一個事實:在 C++ 中,指向不相關類型的成員函數的指標不具有可比性,這與指向 free 和 static 成員函數的指標相反。例如,指向不同類別的虛擬方法的成員函數的指標可以具有相同的位址(它們將方法的偏移量儲存到虛擬函數表中)。
但是,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。
# 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 {}
};
象徵 | 海灣合作委員會 9 Linux 64 大小 | 海灣合作委員會 9 Linux 64 地址 | 微軟VC 16.6 32 大小 | 微軟VC 16.6 32 地址 | 海灣合作委員會 8 明格 32 大小 | 海灣合作委員會 8 明格 32 地址 | 鏗鏘-cl 9 32 大小 | 鏗鏘-cl 9 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::虛擬機 | 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::虛擬機 | 16 | 0x11 | 8 | 0xA911A5 | 8 | 0x09 | 8 | 0x8023AE |
Release模式下的MSVC和Clang-cl透過合併來最佳化具有相同定義的函數。可以使用/OPT:NOICF
連結器選項停用此行為。 Sigslot 測試和範例依賴許多相同的可呼叫函數來觸發此行為,這就是為什麼它在受影響的編譯器上停用此特定最佳化的原因。
在版本低於 7.4 的 GCC 中使用通用 lambda 可能會觸發 Bug #68071。