Sigslot adalah implementasi slot sinyal yang aman untuk header saja dan aman untuk C++.
Tujuan utamanya adalah menggantikan Boost.Signals2.
Terlepas dari fitur-fitur biasa yang ditawarkannya
Sigslot telah diuji unitnya dan harus cukup andal serta stabil untuk menggantikan Boost Signals2.
Pengujian berjalan dengan bersih di bawah alamat, thread, dan pembersih perilaku yang tidak ditentukan.
Banyak implementasi yang mengizinkan jenis pengembalian sinyal, Sigslot tidak melakukannya karena saya tidak menggunakannya. Jika saya dapat yakin akan hal sebaliknya, saya mungkin akan berubah pikiran di kemudian hari.
Tidak diperlukan kompilasi atau instalasi, cukup sertakan sigslot/signal.hpp
dan gunakan. Sigslot saat ini bergantung pada kompiler yang mendukung C++14, tetapi jika diperlukan, ia dapat dipasang ke C++11. Diketahui bekerja dengan kompiler Clang 4.0 dan GCC 5.0+ di GNU Linux, MSVC 2017 dan yang lebih baru, Clang-cl dan MinGW di Windows.
Namun, waspadai potensi kesalahan pada Windows dengan kompiler MSVC dan Clang-Cl, yang mungkin memerlukan tanda tautan /OPT:NOICF
dalam situasi luar biasa. Baca bab Detail Implementasi untuk penjelasannya.
File daftar CMake disediakan untuk tujuan instalasi dan menghasilkan modul impor CMake. Ini adalah metode instalasi yang disukai. Target yang diimpor Pal::Sigslot
telah tersedia dan sudah menerapkan flag linker yang diperlukan. Hal ini juga diperlukan untuk contoh dan pengujian, yang secara opsional bergantung pada Qt5 dan Boost untuk pengujian unit adaptor.
# Using Sigslot from cmake
find_package (PalSigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
Opsi konfigurasi SIGSLOT_REDUCE_COMPILE_TIME
tersedia pada waktu konfigurasi. Saat diaktifkan, ia mencoba mengurangi penggembungan kode dengan menghindari contoh templat yang berat akibat panggilan ke std::make_shared
. Opsi ini tidak aktif secara default, namun dapat diaktifkan bagi mereka yang ingin memilih ukuran kode dan waktu kompilasi pada kode yang sedikit kurang efisien.
Instalasi dapat dilakukan dengan menggunakan instruksi berikut dari direktori root:
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
juga dapat diintegrasikan menggunakan metode 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 mengimplementasikan konstruksi slot sinyal yang populer dalam kerangka UI, sehingga memudahkan penggunaan pola pengamat atau pemrograman berbasis peristiwa. Titik masuk utama perpustakaan adalah templat kelas sigslot::signal<T...>
.
Sinyal adalah objek yang dapat memancarkan notifikasi yang diketik, benar-benar nilai yang diparametrikan setelah parameter templat kelas sinyal, dan mendaftarkan sejumlah penangan notifikasi (callable) dari tipe argumen yang kompatibel untuk dieksekusi dengan nilai yang diberikan setiap kali emisi sinyal terjadi. Dalam istilah slot sinyal, hal ini disebut menghubungkan slot ke sinyal, di mana "slot" mewakili instance yang dapat dipanggil dan "koneksi" dapat dianggap sebagai tautan konseptual dari sinyal ke slot.
Semua cuplikan yang disajikan di bawah ini tersedia dalam bentuk kode sumber yang dapat dikompilasi di subdirektori contoh.
Berikut adalah contoh pertama yang menampilkan fitur paling dasar dari perpustakaan.
Pertama-tama kita mendeklarasikan sig
sinyal bebas parameter, lalu kita lanjutkan menghubungkan beberapa slot dan terakhir memancarkan sinyal yang memicu pemanggilan setiap slot callable yang terhubung sebelumnya. Perhatikan bagaimana perpustakaan menangani beragam bentuk callable.
# 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 ();
}
Secara default, urutan pemanggilan slot saat memancarkan sinyal tidak ditentukan, mohon jangan mengandalkannya untuk selalu sama. Anda dapat membatasi urutan pemanggilan tertentu dengan menggunakan grup slot, yang disajikan nanti.
Contoh pertama itu sederhana namun tidak begitu berguna, mari kita beralih ke sinyal yang memancarkan nilai. Sebuah sinyal dapat mengeluarkan sejumlah argumen, di bawah ini.
# 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);
}
Seperti yang ditunjukkan, tipe argumen slot tidak harus benar-benar identik dengan parameter templat sinyal, karena dapat dikonversi darinya tidak masalah. Argumen umum juga baik-baik saja, seperti yang ditunjukkan pada lambda generik printer
(yang juga dapat ditulis sebagai templat fungsi).
Saat ini ada dua batasan yang dapat saya pikirkan sehubungan dengan penanganan callable: argumen default dan kelebihan fungsi. Keduanya berfungsi dengan benar dalam kasus objek fungsi tetapi akan gagal dikompilasi dengan fungsi statis dan fungsi anggota, karena alasan yang berbeda namun terkait.
Perhatikan potongan kode berikut:
struct foo {
void bar ( double d);
void bar ();
};
Apa yang dimaksud dengan &foo::bar
? Sesuai dengan kelebihan beban, fungsi penunjuk ke anggota ini tidak dipetakan ke simbol unik, sehingga kompiler tidak akan dapat memilih simbol yang tepat. Salah satu cara untuk menyelesaikan simbol yang tepat adalah dengan secara eksplisit mengarahkan penunjuk fungsi ke tipe fungsi yang tepat. Berikut adalah contoh yang melakukan hal itu dengan menggunakan alat pembantu kecil untuk sintaks yang lebih ringan (Sebenarnya saya mungkin akan segera menambahkan ini ke perpustakaan).
# 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 ;
}
Argumen default bukan bagian dari tanda tangan tipe fungsi, dan dapat didefinisikan ulang, sehingga sangat sulit untuk ditangani. Saat menghubungkan slot ke sinyal, perpustakaan menentukan apakah callable yang disediakan dapat dipanggil dengan tipe argumen sinyal, namun pada titik ini keberadaan argumen fungsi default tidak diketahui sehingga mungkin ada ketidakcocokan dalam jumlah argumen.
Solusi sederhana untuk kasus penggunaan ini adalah dengan membuat adaptor pengikat, bahkan kita dapat membuatnya cukup umum seperti:
# 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 ;
}
Apa yang belum terlihat sampai sekarang adalah bahwa signal::connect()
sebenarnya mengembalikan objek sigslot::connection
yang dapat digunakan untuk mengelola perilaku dan masa pakai koneksi slot sinyal. sigslot::connection
adalah objek ringan (pada dasarnya std::weak_ptr
) yang memungkinkan interaksi dengan koneksi slot sinyal yang sedang berlangsung dan memperlihatkan fitur-fitur berikut:
signal::connect()
. sigslot::connection
tidak mengikat koneksi ke suatu cakupan: ini bukan objek RAII, yang menjelaskan mengapa ia dapat disalin. Namun secara implisit dapat diubah menjadi sigslot::scoped_connection
yang menghancurkan koneksi saat keluar dari cakupan.
Berikut adalah contoh yang menggambarkan beberapa fitur tersebut:
# 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 mendukung tanda tangan slot yang diperluas dengan referensi sigslot::connection
tambahan sebagai argumen pertama, yang memungkinkan manajemen koneksi dari dalam slot. Tanda tangan yang diperluas ini dapat diakses menggunakan metode 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
}
Pengguna harus memastikan bahwa masa pakai slot melebihi masa pakai sinyal, yang mungkin membosankan dalam perangkat lunak yang kompleks. Untuk menyederhanakan tugas ini, Sigslot dapat secara otomatis memutuskan sambungan objek slot yang masa hidupnya dapat dilacak. Untuk melakukan itu, slot harus dapat diubah menjadi penunjuk lemah dalam beberapa bentuk.
std::shared_ptr
dan std::weak_ptr
langsung didukung, dan adaptor disediakan untuk mendukung boost::shared_ptr
, boost::weak_ptr
dan Qt QSharedPointer
, QWeakPointer
dan kelas apa pun yang berasal dari QObject
.
Objek lain yang dapat dilacak dapat ditambahkan dengan mendeklarasikan fungsi adaptor 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
}
Cara lain untuk memastikan pemutusan otomatis penunjuk pada slot fungsi anggota adalah dengan mewarisi secara eksplisit dari sigslot::observer
atau sigslot::observer_st
. Yang pertama aman untuk thread, berbeda dengan yang terakhir.
Berikut ini contoh penggunaannya.
# 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
}
Objek yang menggunakan pendekatan intrusif ini mungkin terhubung ke sejumlah sinyal yang tidak berhubungan.
Dukungan untuk pemutusan slot dengan menyediakan tanda tangan fungsi, penunjuk objek, atau pelacak yang sesuai telah diperkenalkan di versi 1.2.0.
Seseorang dapat memutuskan sejumlah slot menggunakan signal::disconnect()
, yang mengusulkan 4 kelebihan beban untuk menentukan kriteria pemutusan:
Pemutusan koneksi lambda hanya dimungkinkan untuk lambda yang terikat pada suatu variabel, karena keunikannya.
Kelebihan beban kedua saat ini memerlukan RTTI untuk memutuskan sambungan dari pointer ke fungsi anggota, objek fungsi, dan lambda. Batasan ini tidak berlaku untuk fungsi anggota gratis dan statis. Alasannya berasal dari fakta bahwa dalam C++, pointer ke fungsi anggota dari tipe yang tidak terkait tidak dapat dibandingkan, berbeda dengan pointer ke fungsi anggota bebas dan statis. Misalnya, penunjuk ke fungsi anggota metode virtual dari kelas yang berbeda dapat memiliki alamat yang sama (mereka menyimpan offset metode ke dalam tabel v).
Namun, Sigslot dapat dikompilasi dengan RTTI dinonaktifkan dan kelebihan beban akan dinonaktifkan untuk kasus yang bermasalah.
Sebagai simpul samping, fitur ini memang menambahkan lebih banyak kode daripada yang diperkirakan pada awalnya karena rumit dan mudah salah. Ini telah dirancang dengan hati-hati, dengan mempertimbangkan kebenaran, dan tidak ada biaya tersembunyi kecuali Anda benar-benar menggunakannya.
Berikut adalah contoh yang mendemonstrasikan fitur tersebut.
# 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 ;
}
Mulai versi 1.2.0, slot dapat diberi id grup untuk mengontrol urutan relatif pemanggilan slot.
Urutan pemanggilan slot dalam grup yang sama tidak ditentukan dan tidak dapat diandalkan, namun grup slot dipanggil dalam urutan id grup menaik. Jika id grup suatu slot tidak disetel, slot tersebut ditetapkan ke grup 0. Id grup dapat memiliki nilai apa pun dalam rentang bilangan bulat 32 bit yang ditandatangani.
# 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 ;
}
Fungsi sigslot::connect()
yang berdiri bebas dapat digunakan untuk menghubungkan sinyal ke sinyal lain dengan argumen yang kompatibel.
# 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 ;
}
Keamanan benang telah diuji unitnya. Secara khusus, emisi sinyal silang dan emisi rekursif berjalan dengan baik dalam skenario multi-thread.
sigslot::signal
adalah typedef dari kelas templat sigslot::signal_base
yang lebih umum, yang argumen templat pertamanya harus berupa tipe yang Dapat Dikunci. Tipe ini akan menentukan kebijakan penguncian kelas.
Sigslot menawarkan 2 typedef,
sigslot::signal
dapat digunakan dari beberapa thread dan menggunakan std::mutex sebagai dapat dikunci. Secara khusus, koneksi, pemutusan, emisi, dan eksekusi slot aman untuk thread. Ini juga aman dengan emisi sinyal rekursif.sigslot::signal_st
adalah alternatif yang tidak aman untuk thread, ini menukar keamanan untuk pengoperasian yang sedikit lebih cepat. Membandingkan pointer fungsi adalah mimpi buruk di C++. Berikut adalah tabel yang menunjukkan ukuran dan alamat berbagai kasus sebagai etalase:
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 {}
};
Simbol | GCC 9 Linux 64 Ukuran dari | GCC 9 Linux 64 Alamat | MSVC 16.6 32 Ukuran dari | MSVC 16.6 32 Alamat | GCC 8 Mingw 32 Ukuran dari | GCC 8 Mingw 32 Alamat | Dentang-cl 9 32 Ukuran dari | Dentang-cl 9 32 Alamat |
---|---|---|---|---|---|---|---|---|
seru | 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 dan Clang-cl dalam mode Rilis mengoptimalkan fungsi dengan definisi yang sama dengan menggabungkannya. Ini adalah perilaku yang dapat dinonaktifkan dengan opsi linker /OPT:NOICF
. Pengujian dan contoh Sigslot sangat bergantung pada callable identik yang memicu perilaku ini, itulah sebabnya ia menonaktifkan pengoptimalan khusus ini pada kompiler yang terpengaruh.
Menggunakan lambda generik dengan GCC kurang dari versi 7.4 dapat memicu Bug #68071.