Sigslot é uma implementação segura de thread somente de cabeçalho de slots de sinal para C++.
O objetivo principal era substituir Boost.Signals2.
Além dos recursos usuais, oferece
Sigslot é testado em unidade e deve ser confiável e estável o suficiente para substituir Boost Signals2.
Os testes são executados de forma limpa sob os sanitizadores de endereço, thread e comportamento indefinido.
Muitas implementações permitem tipos de retorno de sinal, o Sigslot não porque não tenho utilidade para eles. Se eu puder ser convencido do contrário, posso mudar de ideia mais tarde.
Nenhuma compilação ou instalação é necessária, apenas inclua sigslot/signal.hpp
e use-o. Atualmente, o Sigslot depende de um compilador compatível com C++ 14, mas se necessário, ele pode ser adaptado para C++ 11. É conhecido por funcionar com compiladores Clang 4.0 e GCC 5.0+ no GNU Linux, MSVC 2017 e superior, Clang-cl e MinGW no Windows.
No entanto, esteja ciente de uma possível pegadinha no Windows com compiladores MSVC e Clang-Cl, que podem precisar dos sinalizadores de vinculador /OPT:NOICF
em situações excepcionais. Leia o capítulo Detalhes da implementação para obter uma explicação.
Um arquivo de lista CMake é fornecido para fins de instalação e geração de um módulo de importação CMake. Este é o método de instalação preferido. O destino importado Pal::Sigslot
está disponível e já aplica os sinalizadores de linker necessários. Também é necessário para exemplos e testes, que dependem opcionalmente de Qt5 e Boost para testes unitários de adaptadores.
# Using Sigslot from cmake
find_package (PalSigslot)
add_executable (MyExe main.cpp)
target_link_libraries (MyExe PRIVATE Pal::Sigslot)
Uma opção de configuração SIGSLOT_REDUCE_COMPILE_TIME
está disponível no momento da configuração. Quando ativado, ele tenta reduzir o inchaço do código, evitando instanciações pesadas de modelos resultantes de chamadas para std::make_shared
. Esta opção está desativada por padrão, mas pode ser ativada para aqueles que desejam favorecer o tamanho do código e o tempo de compilação em vez de um código um pouco menos eficiente.
A instalação pode ser feita usando as seguintes instruções do diretório raiz:
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
também pode ser integrado usando o 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 a construção de slot de sinal popular em estruturas de UI, facilitando o uso do padrão observador ou da programação baseada em eventos. O principal ponto de entrada da biblioteca é o modelo de classe sigslot::signal<T...>
.
Um sinal é um objeto que pode emitir notificações digitadas, na verdade valores parametrizados após os parâmetros do modelo da classe de sinal, e registrar qualquer número de manipuladores de notificação (chamáveis) de tipos de argumentos compatíveis para serem executados com os valores fornecidos sempre que ocorrer uma emissão de sinal. Na linguagem de slot de sinal, isso é chamado de conexão de um slot a um sinal, onde um "slot" representa uma instância que pode ser chamada e uma "conexão" pode ser considerada um link conceitual de sinal para slot.
Todos os trechos apresentados abaixo estão disponíveis em formato de código-fonte compilável no subdiretório de exemplo.
Aqui está um primeiro exemplo que mostra os recursos mais básicos da biblioteca.
Primeiro declaramos um sinal sem parâmetros sig
, depois conectamos vários slots e, por fim, emitimos um sinal que aciona a invocação de cada slot que pode ser chamado conectado anteriormente. Observe como a biblioteca lida com diversas formas de chamadas.
# 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 ();
}
Por padrão, a ordem de invocação do slot ao emitir um sinal não é especificada, por favor, não confie que ela será sempre a mesma. Você pode restringir uma ordem de invocação específica usando grupos de slots, que serão apresentados posteriormente.
Esse primeiro exemplo foi simples, mas não tão útil, vamos passar para um sinal que emite valores. Um sinal pode emitir qualquer número de argumentos, abaixo.
# 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 mostrado, os tipos de argumentos dos slots não precisam ser estritamente idênticos aos parâmetros do modelo de sinal, sendo conversível é bom. Argumentos genéricos também são adequados, como mostrado com o lambda genérico printer
(que também poderia ter sido escrito como um modelo de função).
No momento, posso pensar em duas limitações em relação ao tratamento que pode ser chamado: argumentos padrão e sobrecarga de funções. Ambos estão funcionando corretamente no caso de objetos de função, mas não conseguirão compilar com funções estáticas e membros, por razões diferentes, mas relacionadas.
Considere o seguinte trecho de código:
struct foo {
void bar ( double d);
void bar ();
};
A que &foo::bar
deve se referir? De acordo com a sobrecarga, esse ponteiro sobre a função membro não é mapeado para um símbolo exclusivo, portanto o compilador não será capaz de escolher o símbolo correto. Uma maneira de resolver o símbolo correto é converter explicitamente o ponteiro de função para o tipo de função correto. Aqui está um exemplo que faz exatamente isso usando uma pequena ferramenta auxiliar para uma sintaxe mais leve (na verdade, provavelmente irei adicionar isso à biblioteca em breve).
# 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 ;
}
Os argumentos padrão não fazem parte da assinatura do tipo de função e podem ser redefinidos, portanto são realmente difíceis de lidar. Ao conectar um slot a um sinal, a biblioteca determina se o callable fornecido pode ser invocado com os tipos de argumento do sinal, mas neste ponto a existência de argumentos de função padrão é desconhecida, portanto pode haver uma incompatibilidade no número de argumentos.
Uma solução simples para este caso de uso seria criar um adaptador de ligação; na verdade, podemos até torná-lo bastante genérico assim:
# 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 ;
}
O que não ficou claro até agora é que signal::connect()
na verdade retorna um objeto sigslot::connection
que pode ser usado para gerenciar o comportamento e a vida útil de uma conexão de slot de sinal. sigslot::connection
é um objeto leve (basicamente um std::weak_ptr
) que permite a interação com uma conexão contínua de slot de sinal e expõe os seguintes recursos:
signal::connect()
. Um sigslot::connection
não vincula uma conexão a um escopo: este não é um objeto RAII, o que explica por que ele pode ser copiado. No entanto, pode ser convertido implicitamente em um sigslot::scoped_connection
que destrói a conexão ao sair do escopo.
Aqui está um exemplo que ilustra alguns desses recursos:
# 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 suporta uma assinatura de slot estendida com uma referência sigslot::connection
adicional como primeiro argumento, que permite o gerenciamento de conexão de dentro do slot. Esta assinatura estendida pode ser acessada usando o 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
}
O usuário deve certificar-se de que a vida útil de um slot exceda a de um sinal, o que pode ser entediante em softwares complexos. Para simplificar esta tarefa, o Sigslot pode desconectar automaticamente o objeto do slot cujo tempo de vida ele é capaz de rastrear. Para fazer isso, o slot deve ser conversível em algum formato de ponteiro fraco.
std::shared_ptr
e std::weak_ptr
são suportados imediatamente, e adaptadores são fornecidos para suportar boost::shared_ptr
, boost::weak_ptr
e Qt QSharedPointer
, QWeakPointer
e qualquer classe derivada de QObject
.
Outros objetos rastreáveis podem ser adicionados declarando uma função 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
}
Outra maneira de garantir a desconexão automática do ponteiro sobre os slots de funções de membro é herdar explicitamente de sigslot::observer
ou sigslot::observer_st
. O primeiro é thread-safe, ao contrário do último.
Aqui está um exemplo 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
}
Os objetos que utilizam esta abordagem intrusiva podem estar conectados a qualquer número de sinais não relacionados.
Suporte para desconexão de slot fornecendo uma assinatura de função apropriada, ponteiro de objeto ou rastreador foi introduzido na versão 1.2.0.
Pode-se desconectar qualquer número de slots usando o método signal::disconnect()
, que propõe 4 sobrecargas para especificar o critério de desconexão:
A desconexão de lambdas só é possível para lambdas vinculadas a uma variável, devido à sua singularidade.
A segunda sobrecarga atualmente precisa do RTTI para se desconectar de ponteiros para funções de membro, objetos de função e lambdas. Esta limitação não se aplica a funções de membro livres e estáticas. As razões decorrem do fato de que em C++, os ponteiros para funções-membro de tipos não relacionados não são comparáveis, ao contrário dos ponteiros para funções-membro livres e estáticas. Por exemplo, o ponteiro para funções-membro de métodos virtuais de classes diferentes pode ter o mesmo endereço (eles armazenam o deslocamento do método na tabela v).
Porém, o Sigslot pode ser compilado com o RTTI desabilitado e a sobrecarga será desativada em casos problemáticos.
Como um nó secundário, esse recurso adicionou mais código do que o previsto inicialmente porque é complicado e fácil de errar. Ele foi projetado cuidadosamente, com a correção em mente, e não possui custos ocultos, a menos que você realmente o utilize.
Aqui está um exemplo que demonstra o recurso.
# 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 da versão 1.2.0, os slots podem receber um ID de grupo para controlar a ordem relativa de invocação dos slots.
A ordem de invocação de slots em um mesmo grupo não é especificada e não deve ser considerada confiável; no entanto, os grupos de slots são invocados em ordem crescente de identificação de grupo. Quando o ID do grupo de um slot não é definido, ele é atribuído ao grupo 0. Os IDs do grupo podem ter qualquer valor no intervalo de números inteiros assinados de 32 bits.
# 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 ;
}
A função independente sigslot::connect()
pode ser usada para conectar um sinal a outro com argumentos compatíveis.
# 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 ;
}
A segurança do thread é testada em unidade. Em particular, a emissão de sinal cruzado e a emissão recursiva funcionam bem em um cenário de múltiplos threads.
sigslot::signal
é um typedef para a classe de modelo mais geral sigslot::signal_base
, cujo primeiro argumento de modelo deve ser do tipo Lockable. Este tipo ditará a política de bloqueio da classe.
Sigslot oferece 2 typedefs,
sigslot::signal
utilizável em vários threads e usa std::mutex como bloqueável. Em particular, conexão, desconexão, emissão e execução de slot são thread-safe. Também é seguro com emissão de sinal recursiva.sigslot::signal_st
é uma alternativa não segura para threads, pois troca segurança por uma operação um pouco mais rápida. Comparar ponteiros de função é um pesadelo em C++. Aqui está uma tabela que demonstra o tamanho e o endereço de uma variedade de caixas como vitrine:
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 | GCC 9 Linux 64 Tamanho de | GCC 9 Linux 64 Endereço | MSVC 16.6 32 Tamanho de | MSVC 16.6 32 Endereço | CCG 8 Mingw 32 Tamanho de | CCG 8 Mingw 32 Endereço | Clang-cl 9 32 Tamanho de | Clang-cl 9 32 Endereço |
---|---|---|---|---|---|---|---|---|
diversão | 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 e Clang-cl no modo Release otimizam funções com a mesma definição, mesclando-as. Este é um comportamento que pode ser desativado com a opção do vinculador /OPT:NOICF
. Os testes e exemplos do Sigslot dependem de muitos callables idênticos que desencadeiam esse comportamento, e é por isso que ele desativa essa otimização específica nos compiladores afetados.
Usar lambdas genéricos com GCC inferior à versão 7.4 pode acionar o Bug #68071.