Sigslot — это потокобезопасная реализация сигнальных слотов для C++, предназначенная только для заголовков.
Основной целью было заменить Boost.Signals2.
Помимо обычных функций, он предлагает
Sigslot прошел модульное тестирование и должен быть достаточно надежным и стабильным, чтобы заменить Boost Signals2.
Тесты выполняются без ошибок под дезинфицирующими средствами адреса, потока и неопределенного поведения.
Многие реализации допускают типы возврата сигналов, а Sigslot — нет, потому что они мне не нужны. Если я смогу убедиться в обратном, возможно, позже я изменю свое мнение.
Никакой компиляции или установки не требуется, просто подключите sigslot/signal.hpp
и используйте его. Sigslot в настоящее время зависит от компилятора, совместимого с C++14, но при необходимости он может быть модифицирован до C++11. Известно, что он работает с компиляторами Clang 4.0 и GCC 5.0+ в GNU Linux, MSVC 2017 и более поздних версиях, Clang-cl и MinGW в Windows.
Однако имейте в виду потенциальную ошибку в Windows с компиляторами MSVC и Clang-Cl, которым в исключительных ситуациях могут потребоваться флаги компоновщика /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 реализует конструкцию сигнального слота, популярную в средах пользовательского интерфейса, что упрощает использование шаблона наблюдателя или программирование на основе событий. Основной точкой входа в библиотеку является шаблон класса 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
(которая также могла бы быть записана как шаблон функции).
На данный момент я могу придумать два ограничения в отношении обработки вызываемых объектов: аргументы по умолчанию и перегрузка функций. Оба работают правильно в случае объектов-функций, но не могут скомпилироваться со статическими функциями и функциями-членами по разным, но связанным причинам.
Рассмотрим следующий фрагмент кода:
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 перегрузки для указания критерия отключения:
Отключение лямбд возможно только для лямбд, привязанных к переменной, ввиду их уникальности.
Вторая перегрузка в настоящее время требует, чтобы 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, слотам можно назначать идентификатор группы, чтобы контролировать относительный порядок вызова слотов.
Порядок вызова слотов в одной группе не указан, и на него не следует полагаться, однако группы слотов вызываются в порядке возрастания идентификатора группы. Если идентификатор группы слота не установлен, он назначается группе 0. Идентификаторы групп могут иметь любое значение в диапазоне 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
, первый аргумент шаблона которого должен быть блокируемым типом. Этот тип будет определять политику блокировки класса.
Sigslot предлагает 2 определения типов,
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 Адрес | МСВК 16,6 32 Размер | МСВК 16,6 32 Адрес | GCC 8 Минв 32 Размер | GCC 8 Минв 32 Адрес | Clang-cl 9 32 Размер | Clang-cl 9 32 Адрес |
---|---|---|---|---|---|---|---|---|
веселье | 8 | 0x802340 | 4 | 0x1311A6 | 4 | 0xF41540 | 4 | 0x0010AE |
&b1::см | 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::см | 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 |
&д::см | 8 | 0x203440 | 4 | 0x2612A5 | 4 | 0x108D40 | 4 | 0x0010AE |
&д::м | 16 | 0x303540 | 4 | 0x9D13A5 | 8 | 0x048D40 | 4 | 0x0010AE |
&д::вм | 16 | 0x11 | 4 | 0x4412A5 | 8 | 0x09 | 4 | 0x8023AE |
&e::см | 8 | 0x403540 | 4 | 0xF911A5 | 4 | 0x208D40 | 4 | 0x0010AE |
&Эм | 16 | 0x503640 | 8 | 0x8111A5 | 8 | 0x148D40 | 8 | 0x0010AE |
&e::vm | 16 | 0x11 | 8 | 0xA911A5 | 8 | 0x09 | 8 | 0x8023AE |
MSVC и Clang-cl в режиме Release оптимизируют функции с одинаковым определением путем их слияния. Это поведение можно отключить с помощью параметра компоновщика /OPT:NOICF
. Тесты и примеры Sigslot полагаются на множество одинаковых вызовов, которые вызывают такое поведение, поэтому он деактивирует эту конкретную оптимизацию на затронутых компиляторах.
Использование общих лямбда-выражений с GCC версии ниже 7.4 может вызвать ошибку № 68071.