Sigslot은 C++용 신호 슬롯의 헤더 전용 스레드 안전 구현입니다.
주요 목표는 Boost.Signals2를 대체하는 것이었습니다.
일반적인 기능 외에도 다음과 같은 기능을 제공합니다.
Sigslot은 단위 테스트를 거쳤으며 Boost Signals2를 대체할 만큼 충분히 안정적이고 안정적이어야 합니다.
테스트는 주소, 스레드 및 정의되지 않은 동작 살균제에서 깔끔하게 실행됩니다.
많은 구현에서는 신호 반환 유형을 허용하지만 Sigslot은 사용할 필요가 없기 때문에 허용하지 않습니다. 그렇지 않다고 확신할 수 있다면 나중에 마음이 바뀔 수도 있습니다.
컴파일이나 설치가 필요하지 않습니다. sigslot/signal.hpp
포함하고 사용하세요. Sigslot은 현재 C++14 호환 컴파일러에 의존하지만 필요한 경우 C++11로 개조될 수 있습니다. GNU Linux, MSVC 2017 이상, Windows의 Clang-cl 및 MinGW에서는 Clang 4.0 및 GCC 5.0+ 컴파일러와 함께 작동하는 것으로 알려져 있습니다.
그러나 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
일반 람다(함수 템플릿으로도 작성되었을 수도 있음)에서 볼 수 있듯이 일반 인수도 괜찮습니다.
현재 호출 가능 처리와 관련하여 제가 생각할 수 있는 두 가지 제한 사항은 기본 인수와 함수 오버로딩입니다. 둘 다 함수 개체의 경우 올바르게 작동하지만 서로 다르지만 관련된 이유로 정적 및 멤버 함수로 컴파일하지 못합니다.
다음 코드를 고려해보세요.
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에 도입되었습니다.
연결 해제 기준을 지정하기 위해 4개의 오버로드를 제안하는 signal::disconnect()
메서드를 사용하여 슬롯 수에 관계없이 연결을 끊을 수 있습니다.
람다 연결 끊기는 고유성으로 인해 변수에 바인딩된 람다에만 가능합니다.
두 번째 오버로드에는 현재 멤버 함수, 함수 개체 및 람다에 대한 포인터 연결을 끊기 위해 RTTI가 필요합니다. 이 제한은 자유 및 정적 멤버 함수에는 적용되지 않습니다. 그 이유는 C++에서 자유 및 정적 멤버 함수에 대한 포인터와 달리 관련되지 않은 유형의 멤버 함수에 대한 포인터를 비교할 수 없기 때문입니다. 예를 들어, 서로 다른 클래스의 가상 메소드의 멤버 함수에 대한 포인터는 동일한 주소를 가질 수 있습니다(메서드의 오프셋을 vtable에 저장하는 것과 같습니다).
그러나 RTTI가 비활성화된 상태에서 Sigslot을 컴파일할 수 있으며 문제가 있는 경우 오버로드가 비활성화됩니다.
사이드 노드로서 이 기능은 까다롭고 잘못되기 쉽기 때문에 처음에 예상했던 것보다 더 많은 코드를 추가한 것으로 인정됩니다. 정확성을 염두에 두고 신중하게 설계되었으며 실제로 사용하지 않는 한 숨겨진 비용이 없습니다.
다음은 해당 기능을 보여주는 예입니다.
# 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에 할당됩니다. 그룹 ID는 부호 있는 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
첫 번째 템플릿 인수가 Lockable 유형이어야 하는 보다 일반적인 sigslot::signal_base
템플릿 클래스에 대한 형식 정의입니다. 이 유형은 클래스의 잠금 정책을 지정합니다.
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 {}
};
상징 | GCC 9 리눅스 64 크기 | GCC 9 리눅스 64 주소 | MSVC 16.6 32 크기 | MSVC 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::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 |
&여자 이름 | 16 | 0x503640 | 8 | 0x8111A5 | 8 | 0x148D40 | 8 | 0x0010AE |
&e::vm | 16 | 0x11 | 8 | 0xA911A5 | 8 | 0x09 | 8 | 0x8023AE |
릴리스 모드의 MSVC와 Clang-cl은 병합하여 동일한 정의로 기능을 최적화합니다. 이는 /OPT:NOICF
링커 옵션을 사용하여 비활성화할 수 있는 동작입니다. Sigslot 테스트와 예제는 이 동작을 트리거하는 동일한 호출 가능 항목에 의존하므로 영향을 받는 컴파일러에서 이 특정 최적화를 비활성화합니다.
버전 7.4 미만의 GCC에서 일반 람다를 사용하면 버그 #68071이 발생할 수 있습니다.