Sigslot es una implementación segura para subprocesos y solo encabezado de ranuras de señal para C++.
El objetivo principal era reemplazar Boost.Signals2.
Además de las características habituales, ofrece
Sigslot está probado en unidades y debe ser lo suficientemente confiable y estable como para reemplazar Boost Signals2.
Las pruebas se ejecutan limpiamente bajo los desinfectantes de dirección, subproceso y comportamiento indefinido.
Muchas implementaciones permiten tipos de retorno de señal, Sigslot no porque no los uso. Si puedo convencerme de lo contrario, puedo cambiar de opinión más adelante.
No se requiere compilación ni instalación, simplemente incluya sigslot/signal.hpp
y utilícelo. Actualmente, Sigslot depende de un compilador compatible con C++ 14, pero si surge la necesidad, puede actualizarse a C++ 11. Se sabe que funciona con los compiladores Clang 4.0 y GCC 5.0+ en GNU Linux, MSVC 2017 y versiones posteriores, Clang-cl y MinGW en Windows.
Sin embargo, tenga en cuenta un posible problema en Windows con los compiladores MSVC y Clang-Cl, que pueden necesitar los indicadores del vinculador /OPT:NOICF
en situaciones excepcionales. Lea el capítulo Detalles de implementación para obtener una explicación.
Se proporciona un archivo de lista de CMake para fines de instalación y generación de un módulo de importación de CMake. Este es el método de instalación preferido. El objetivo importado Pal::Sigslot
está disponible y ya aplica los indicadores del vinculador necesarios. También es necesario para ejemplos y pruebas, que opcionalmente dependen de Qt5 y Boost para pruebas unitarias de adaptadores.
# Using Sigslot from cmake
find_package (PalSigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
Una opción de configuración SIGSLOT_REDUCE_COMPILE_TIME
está disponible en el momento de la configuración. Cuando se activa, intenta reducir la sobrecarga de código evitando la creación de grandes instancias de plantillas resultantes de llamadas a std::make_shared
. Esta opción está desactivada de forma predeterminada, pero se puede activar para aquellos que deseen favorecer el tamaño del código y el tiempo de compilación en lugar de un código ligeramente menos eficiente.
La instalación se puede realizar siguiendo las siguientes instrucciones desde el directorio raíz:
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
también se puede integrar utilizando el método 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 implementa la construcción de ranura de señal popular en los marcos de UI, lo que facilita el uso del patrón de observador o la programación basada en eventos. El principal punto de entrada de la biblioteca es la plantilla de clase sigslot::signal<T...>
.
Una señal es un objeto que puede emitir notificaciones escritas, en realidad valores parametrizados después de los parámetros de la plantilla de clase de señal, y registrar cualquier número de controladores de notificaciones (invocables) de tipos de argumentos compatibles para ejecutarse con los valores proporcionados cada vez que ocurre una emisión de señal. En el lenguaje de ranuras de señales, esto se denomina conectar una ranura a una señal, donde una "ranura" representa una instancia invocable y una "conexión" puede considerarse como un vínculo conceptual de la señal a la ranura.
Todos los fragmentos que se presentan a continuación están disponibles en formato de código fuente compilable en el subdirectorio de ejemplo.
A continuación se muestra un primer ejemplo que muestra las características más básicas de la biblioteca.
Primero declaramos una sig
sin parámetros, luego procedemos a conectar varias ranuras y finalmente emitimos una señal que activa la invocación de cada ranura invocable conectada de antemano. Observe cómo la biblioteca maneja diversas formas de invocables.
# 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 ();
}
De forma predeterminada, el orden de invocación de ranuras al emitir una señal no está especificado; no confíe en que sea siempre el mismo. Puede restringir un orden de invocación particular utilizando grupos de espacios, que se presentan más adelante.
Ese primer ejemplo fue simple pero no tan útil; pasemos a una señal que emite valores. Una señal puede emitir cualquier cantidad de argumentos, a continuación.
# 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);
}
Como se muestra, los tipos de argumentos de slots no necesitan ser estrictamente idénticos a los parámetros de la plantilla de señal; ser convertibles está bien. Los argumentos genéricos también están bien, como se muestra con la lambda genérica printer
(que también podría haberse escrito como una plantilla de función).
En este momento hay dos limitaciones que se me ocurren con respecto al manejo de invocables: argumentos predeterminados y sobrecarga de funciones. Ambos funcionan correctamente en el caso de objetos de función, pero no podrán compilarse con funciones estáticas y miembro, por razones diferentes pero relacionadas.
Considere el siguiente fragmento de código:
struct foo {
void bar ( double d);
void bar ();
};
¿A qué debería referirse &foo::bar
? Según la sobrecarga, este puntero sobre la función miembro no se asigna a un símbolo único, por lo que el compilador no podrá elegir el símbolo correcto. Una forma de resolver el símbolo correcto es convertir explícitamente el puntero de función al tipo de función correcto. Aquí hay un ejemplo que hace precisamente eso usando una pequeña herramienta auxiliar para una sintaxis más ligera (de hecho, probablemente agregaré esto a la biblioteca pronto).
# 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 ;
}
Los argumentos predeterminados no forman parte de la firma del tipo de función y se pueden redefinir, por lo que son realmente difíciles de manejar. Al conectar una ranura a una señal, la biblioteca determina si el invocable proporcionado se puede invocar con los tipos de argumentos de señal, pero en este punto se desconoce la existencia de argumentos de función predeterminados, por lo que puede haber una discrepancia en el número de argumentos.
Una solución sencilla para este caso de uso sería crear un adaptador de enlace; de hecho, incluso podemos hacerlo bastante genérico así:
# 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 ;
}
Lo que no se hizo evidente hasta ahora es que signal::connect()
en realidad devuelve un objeto sigslot::connection
que puede usarse para administrar el comportamiento y la vida útil de una conexión de ranura de señal. sigslot::connection
es un objeto liviano (básicamente un std::weak_ptr
) que permite la interacción con una conexión de ranura de señal en curso y expone las siguientes características:
signal::connect()
. Una sigslot::connection
no vincula una conexión a un alcance: este no es un objeto RAII, lo que explica por qué se puede copiar. Sin embargo, se puede convertir implícitamente en sigslot::scoped_connection
que destruye la conexión cuando sale del alcance.
A continuación se muestra un ejemplo que ilustra algunas de esas características:
# 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 admite una firma de ranura extendida con una referencia sigslot::connection
adicional como primer argumento, lo que permite la gestión de la conexión desde el interior de la ranura. Se puede acceder a esta firma extendida mediante el método 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
}
El usuario debe asegurarse de que la vida útil de una ranura supere la de una señal, lo que puede resultar tedioso en software complejo. Para simplificar esta tarea, Sigslot puede desconectar automáticamente el objeto de ranura cuya vida útil puede rastrear. Para hacer eso, la ranura debe ser convertible a un puntero débil de algún tipo.
std::shared_ptr
y std::weak_ptr
son compatibles de fábrica y se proporcionan adaptadores para admitir boost::shared_ptr
, boost::weak_ptr
y Qt QSharedPointer
, QWeakPointer
y cualquier clase derivada de QObject
.
Se pueden agregar otros objetos rastreables declarando una función de adaptador 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
}
Otra forma de garantizar la desconexión automática del puntero sobre las ranuras de funciones miembro es heredando explícitamente de sigslot::observer
o sigslot::observer_st
. El primero es seguro para subprocesos, al contrario que el segundo.
A continuación se muestra un ejemplo de uso.
# 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
}
Los objetos que utilizan este enfoque intrusivo pueden estar conectados a cualquier número de señales no relacionadas.
En la versión 1.2.0 se introdujo la compatibilidad con la desconexión de ranuras mediante el suministro de una firma de función, un puntero de objeto o un rastreador apropiado.
Se puede desconectar cualquier número de slots utilizando el método signal::disconnect()
, que propone 4 sobrecargas para especificar el criterio de desconexión:
La desconexión de lambdas solo es posible para lambdas vinculadas a una variable, debido a su unicidad.
La segunda sobrecarga actualmente necesita que RTTI se desconecte de los punteros a funciones miembro, objetos de función y lambdas. Esta limitación no se aplica a las funciones de miembros estáticas y gratuitas. La razón surge del hecho de que en C++, los punteros a funciones miembro de tipos no relacionados no son comparables, a diferencia de los punteros a funciones miembro libres y estáticas. Por ejemplo, el puntero a funciones miembro de métodos virtuales de diferentes clases puede tener la misma dirección (de alguna manera almacenan el desplazamiento del método en la vtable).
Sin embargo, Sigslot se puede compilar con RTTI deshabilitado y la sobrecarga se desactivará para casos problemáticos.
Como nodo lateral, es cierto que esta característica agregó más código de lo previsto al principio porque es complicado y fácil de equivocar. Ha sido diseñado cuidadosamente, teniendo en cuenta la corrección, y no tiene costos ocultos a menos que realmente lo use.
A continuación se muestra un ejemplo que demuestra la característica.
# 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 ;
}
A partir de la versión 1.2.0, a las ranuras se les puede asignar una identificación de grupo para controlar el orden relativo de invocación de las ranuras.
El orden de invocación de espacios en un mismo grupo no está especificado y no se debe confiar en él; sin embargo, los grupos de espacios se invocan en orden ascendente de identificación de grupo. Cuando la identificación de grupo de una ranura no está configurada, se asigna al grupo 0. Las identificaciones de grupo pueden tener cualquier valor en el rango de enteros de 32 bits con signo.
# 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 ;
}
La función independiente sigslot::connect()
se puede utilizar para conectar una señal a otra con argumentos compatibles.
# 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 ;
}
La seguridad de los hilos se prueba en unidades. En particular, la emisión de señales cruzadas y la emisión recursiva funcionan bien en un escenario de múltiples subprocesos.
sigslot::signal
es un typedef para la clase de plantilla sigslot::signal_base
más general, cuyo primer argumento de plantilla debe ser un tipo Bloqueable. Este tipo dictará la política de bloqueo de la clase.
Sigslot ofrece 2 tipos de definición,
sigslot::signal
utilizable desde múltiples subprocesos y usa std::mutex como bloqueable. En particular, la conexión, desconexión, emisión y ejecución de ranuras son seguras para subprocesos. También es seguro con la emisión de señales recursivas.sigslot::signal_st
es una alternativa que no es segura para subprocesos, intercambia seguridad por una operación ligeramente más rápida. Comparar punteros de función es una pesadilla en C++. A continuación se muestra una tabla que muestra el tamaño y la dirección de una variedad de casos a modo de muestra:
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 {}
};
Símbolo | CCG 9 Linux 64 Tamaño de | CCG 9 Linux 64 DIRECCIÓN | MSVC 16.6 32 Tamaño de | MSVC 16.6 32 DIRECCIÓN | CCG 8 Mingw 32 Tamaño de | CCG 8 Mingw 32 DIRECCIÓN | Clang-cl 9 32 Tamaño de | Clang-cl 9 32 DIRECCIÓN |
---|---|---|---|---|---|---|---|---|
divertido | 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::máquina virtual | 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 y Clang-cl en modo Release optimizan funciones con la misma definición fusionándolas. Este es un comportamiento que se puede desactivar con la opción del vinculador /OPT:NOICF
. Las pruebas y ejemplos de Sigslot se basan en gran medida en invocables idénticos que desencadenan este comportamiento, por lo que desactiva esta optimización particular en los compiladores afectados.
El uso de lambdas genéricas con GCC anterior a la versión 7.4 puede desencadenar el error n.º 68071.