Qt中的信号与槽机制很好用,然而只在Qt环境中。在现代 C++ 编程中,对象间的通信是一个核心问题。为了解决这个问题,许多库提供了信号和槽(Signals and Slots)机制。今天推荐分享一个轻量级的实现:
sigslot
库。源码也很小,用来学习c++的新特性也是不错的选择。
介绍
sigslot
是一个轻量级的 C++ 信号和槽库,它提供了一种类型安全的机制来处理对象之间的通信。信号和槽机制允许对象在状态变化时通知其他对象,而无需直接调用它们的成员函数。这种机制有助于减少对象之间的耦合,使代码更易于维护和扩展。
仓库地址
你可以在 GitHub 上找到 sigslot
库的源码: sigslot GitHub 仓库
https://github.com/palacaze/sigslot
优缺点
优点
- 类型安全:
sigslot
提供了编译时的类型检查,确保信号和槽之间的参数类型匹配。 - 多线程支持:
sigslot
支持多线程环境,可以安全地在不同线程之间传递信号。 - 自动连接管理:
sigslot
会自动管理信号和槽之间的连接,当对象被销毁时,相关的连接也会自动断开。 - 灵活性:
sigslot
允许一个信号连接到多个槽,也允许一个槽连接到多个信号。 - 简单易用:
sigslot
的 API 设计简洁,易于理解和使用。
缺点
- 功能相对简单:相比于
Boost.Signals2
或Qt
的信号和槽机制,sigslot
的功能较为简单,可能不适合需要复杂信号和槽机制的项目。 - 文档和社区支持有限:作为一个相对小众的库,
sigslot
的文档和社区支持可能不如一些主流库那么丰富。
sigslot作用
Sigslot是信号(signal)和槽(slot)的结合,是一种用于处理C++对象通信的机制。信号是一个对象发出的事件或状态的通知,而槽则是响应信号并执行特定动作的函数。
Sigslot 的作用一句话表式就是为了解耦。例如,有两个类 A 和 B,如果 B 使用 A, 就必须在 B 类中写入与 A 类有关的代码。
使用Sigslot的主要原因包括:
- 解耦对象之间的通信:Sigslot可以帮助对象完全独立通信,减少对象之间的耦合度,提高程序的可维护性和可扩展性。
- 简化对象之间的交互:Sigslot可以让对象之间的交互变得更加灵活和简单,使得代码更易于阅读和维护。
- 支持事件驱动编程:Sigslot可以方便地实现事件驱动的编程模式,使得代码结构清晰,易于理解。
总的来说,Sigslot可以帮助简化C++对象之间的通信和交互,使得代码更加清晰和可维护。
实现原理
sigslot的原理其实非常简单,它就是一个变化的观察者模式。观察者模式如下所示:
观察者模式,首先让 Observer(“观察者”)对象 注册到 Subject(“被观察者”) 对象中。当 Subject 状态发生变化时,遍历所有注册到自己的 Observer 对象,并调用它们的 notify方法。
sigslot与观察者模式类似,它使用signal(“信号”)和slot("槽"),区别在于 signal 主动连接自己感兴趣的类及其方法,将它们保存到自己的列表中。当发射信号时,它遍历所有的连接,调用 slot(“槽”) 方法。
源码实现
#pragma once
#include <atomic>
#include <cstring>
#include <memory>
#include <mutex>
#include <type_traits>
#include <utility>
#include <thread>
#include <vector>
#if defined(__GXX_RTTI) || defined(__cpp_rtti) || defined(_CPPRTTI)
#define SIGSLOT_RTTI_ENABLED 1
#include <typeinfo>
#endif
namespace sigslot {
namespace detail {
// Used to detect an object of observer type
struct observer_type {};
} // namespace detail
namespace trait {
/// represent a list of types
template <typename...> struct typelist {};
/**
* Pointers that can be converted to a weak pointer concept for tracking
* purpose must implement the to_weak() function in order to make use of
* ADL to convert that type and make it usable
*/
template <typename T>
std::weak_ptr<T> to_weak(std::weak_ptr<T> w) {
return w;
}
template <typename T>
std::weak_ptr<T> to_weak(std::shared_ptr<T> s) {
return s;
}
// tools
namespace detail {
template <typename...>
struct voider { using type = void; };
// void_t from c++17
template <typename...T>
using void_t = typename detail::voider<T...>::type;
template <typename, typename = void>
struct has_call_operator : std::false_type {};
template <typename F>
struct has_call_operator<F, void_t<decltype(&std::remove_reference<F>::type::operator())>>
: std::true_type {};
template <typename, typename, typename = void, typename = void>
struct is_callable : std::false_type {};
template <typename F, typename P, typename... T>
struct is_callable<F, P, typelist<T...>,
void_t<decltype(((*std::declval<P>()).*std::declval<F>())(std::declval<T>()...))>>
: std::true_type {};
template <typename F, typename... T>
struct is_callable<F, typelist<T...>,
void_t<decltype(std::declval<F>()(std::declval<T>()...))>>
: std::true_type {};
template <typename T, typename = void>
struct is_weak_ptr : std::false_type {};
template <typename T>
struct is_weak_ptr<T, void_t<decltype(std::declval<T>().expired()),
decltype(std::declval<T>().lock()),
decltype(std::declval<T>().reset())>>
: std::true_type {};
template <typename T, typename = void>
struct is_weak_ptr_compatible : std::false_type {};
template <typename T>
struct is_weak_ptr_compatible<T, void_t<decltype(to_weak(std::declval<T>()))>>
: is_weak_ptr<decltype(to_weak(std::declval<T>()))> {};
} // namespace detail
static constexpr bool with_rtti =
#ifdef SIGSLOT_RTTI_ENABLED
true;
#else
false;
#endif
/// determine if a pointer is convertible into a "weak" pointer
template <typename P>
constexpr bool is_weak_ptr_compatible_v = detail::is_weak_ptr_compatible<std::decay_t<P>>::value;
/// determine if a type T (Callable or Pmf) is callable with supplied arguments
template <typename L, typename... T>
constexpr bool is_callable_v = detail::is_callable<T..., L>::value;
template <typename T>
constexpr bool is_weak_ptr_v = detail::is_weak_ptr<T>::value;
template <typename T>
constexpr bool has_call_operator_v = detail::has_call_operator<T>::value;
template <typename T>
constexpr bool is_pointer_v = std::is_pointer<T>::value;
template <typename T>
constexpr bool is_func_v = std::is_function<T>::value;
template <typename T>
constexpr bool is_pmf_v = std::is_member_function_pointer<T>::value;
template <typename T>
constexpr bool is_observer_v = std::is_base_of<::sigslot::detail::observer_type,
std::remove_pointer_t<std::remove_reference_t<T>>>::value;
} // namespace trait
template <typename, typename...>
class signal_base;
/**
* A group_id is used to identify a group of slots
*/
using group_id = std::int32_t;
namespace detail {
/**
* The following function_traits and object_pointer series of templates are
* used to circumvent the type-erasing that takes place in the slot_base
* implementations. They are used to compare the stored functions and objects
* with another one for disconnection purpose.
*/
/*
* Function pointers and member function pointers size differ from compiler to
* compiler, and for virtual members compared to non virtual members. On some
* compilers, multiple inheritance has an impact too. Hence, we form an union
* big enough to store any kind of function pointer.
*/
namespace mock {
struct a { virtual ~a() = default; void f(); virtual void g(); static void h(); };
struct b { virtual ~b() = default; void f(); virtual void g(); };
struct c : a, b { void f(); void g() override; };
struct d : virtual a { void g() override; };
union fun_types {
decltype(&d::g) dm;
decltype(&c::g) mm;
decltype(&c::g) mvm;
decltype(&a::f) m;
decltype(&a::g) vm;
decltype(&a::h) s;
void (*f)();
void *o;
};
} // namespace mock
/*
* This struct is used to store function pointers.
* This is needed for slot disconnection by function pointer.
* It assumes the underlying implementation to be trivially copiable.
*/
struct func_ptr {
func_ptr()
: sz{0}
{
std::uninitialized_fill(std::begin(data), std::end(data), '\0');
}
template <typename T>
void store(const T &t) {
const auto *b = reinterpret_cast<const char*>(&t);
sz = sizeof(T);
std::memcpy(data, b, sz);
}
template <typename T>
const T* as() const {
if (sizeof(T) != sz) {
return nullptr;
}
return reinterpret_cast<const T*>(data);
}
private:
alignas(sizeof(mock::fun_types)) char data[sizeof(mock::fun_types)];
size_t sz;
};
template <typename T, typename = void>
struct function_traits {
static void ptr(const T &/*t*/, func_ptr &/*d*/) {
}
static bool eq(const T &/*t*/, const func_ptr &/*d*/) {
return false;
}
static constexpr bool is_disconnectable = false;
static constexpr bool must_check_object = true;
};
template <typename T>
struct function_traits<T, std::enable_if_t<trait::is_func_v<T>>> {
static void ptr(T &t, func_ptr &d) {
d.store(&t);
}
static bool eq(T &t, const func_ptr &d) {
const auto *r = d.as<const T*>();
return r && *r == &t;
}
static constexpr bool is_disconnectable = true;
static constexpr bool must_check_object = false;
};
template <typename T>
struct function_traits<T*, std::enable_if_t<trait::is_func_v<T>>> {
static void ptr(T *t, func_ptr &d) {
function_traits<T>::ptr(*t, d);
}
static bool eq(T *t, const func_ptr &d) {
return function_traits<T>::eq(*t, d);
}
static constexpr bool is_disconnectable = true;
static constexpr bool must_check_object = false;
};
template <typename T>
struct function_traits<T, std::enable_if_t<trait::is_pmf_v<T>>> {
static void ptr(T t, func_ptr &d) {
d.store(t);
}
static bool eq(T t, const func_ptr &d) {
const auto *r = d.as<const T>();
return r && *r == t;
}
static constexpr bool is_disconnectable = trait::with_rtti;
static constexpr bool must_check_object = true;
};
// for function objects, the assumption is that we are looking for the call operator
template <typename T>
struct function_traits<T, std::enable_if_t<trait::has_call_operator_v<T>>> {
using call_type = decltype(&std::remove_reference<T>::type::operator());
static void ptr(const T &/*t*/, func_ptr &d) {
function_traits<call_type>::ptr(&T::operator(), d);
}
static bool eq(const T &/*t*/, const func_ptr &d) {
return function_traits<call_type>::eq(&T::operator(), d);
}
static constexpr bool is_disconnectable = function_traits<call_type>::is_disconnectable;
static constexpr bool must_check_object = function_traits<call_type>::must_check_object;
};
template <typename T>
func_ptr get_function_ptr(const T &t) {
func_ptr d;
function_traits<std::decay_t<T>>::ptr(t, d);
return d;
}
template <typename T>
bool eq_function_ptr(const T& t, const func_ptr &d) {
return function_traits<std::decay_t<T>>::eq(t, d);
}
/*
* obj_ptr is used to store a pointer to an object.
* The object_pointer traits are needed to handle trackable objects correctly,
* as they are likely to not be pointers.
*/
using obj_ptr = const void*;
template <typename T>
obj_ptr get_object_ptr(const T &t);
template <typename T, typename = void>
struct object_pointer {
static obj_ptr get(const T&) {
return nullptr;
}
};
template <typename T>
struct object_pointer<T*, std::enable_if_t<trait::is_pointer_v<T*>>> {
static obj_ptr get(const T *t) {
return reinterpret_cast<obj_ptr>(t);
}
};
template <typename T>
struct object_pointer<T, std::enable_if_t<trait::is_weak_ptr_v<T>>> {
static obj_ptr get(const T &t) {
auto p = t.lock();
return get_object_ptr(p);
}
};
template <typename T>
struct object_pointer<T, std::enable_if_t<!trait::is_pointer_v<T> &&
!trait::is_weak_ptr_v<T> &&
trait::is_weak_ptr_compatible_v<T>>>
{
static obj_ptr get(const T &t) {
return t ? reinterpret_cast<obj_ptr>(t.get()) : nullptr;
}
};
template <typename T>
obj_ptr get_object_ptr(const T &t) {
return object_pointer<T>::get(t);
}
// noop mutex for thread-unsafe use
struct null_mutex {
null_mutex() noexcept = default;
~null_mutex() noexcept = default;
null_mutex(const null_mutex &) = delete;
null_mutex& operator=(const null_mutex &) = delete;
null_mutex(null_mutex &&) = delete;
null_mutex& operator=(null_mutex &&) = delete;
inline bool try_lock() noexcept { return true; }
inline void lock() noexcept {}
inline void unlock() noexcept {}
};
/**
* A spin mutex that yields, mostly for use in benchmarks and scenarii that invoke
* slots at a very high pace.
* One should almost always prefer a standard mutex over this.
*/
struct spin_mutex {
spin_mutex() noexcept = default;
~spin_mutex() noexcept = default;
spin_mutex(spin_mutex const&) = delete;
spin_mutex& operator=(const spin_mutex &) = delete;
spin_mutex(spin_mutex &&) = delete;
spin_mutex& operator=(spin_mutex &&) = delete;
void lock() noexcept {
while (true) {
while (!state.load(std::memory_order_relaxed)) {
std::this_thread::yield();
}
if (try_lock()) {
break;
}
}
}
bool try_lock() noexcept {
return state.exchange(false, std::memory_order_acquire);
}
void unlock() noexcept {
state.store(true, std::memory_order_release);
}
private:
std::atomic<bool> state {true};
};
/**
* A simple copy on write container that will be used to improve slot lists
* access efficiency in a multithreaded context.
*/
template <typename T>
class copy_on_write {
struct payload {
payload() = default;
template <typename... Args>
explicit payload(Args && ...args)
: value(std::forward<Args>(args)...)
{}
std::atomic<std::size_t> count{1};
T value;
};
public:
using element_type = T;
copy_on_write()
: m_data(new payload)
{}
template <typename U>
explicit copy_on_write(U && x, std::enable_if_t<!std::is_same<std::decay_t<U>,
copy_on_write>::value>* = nullptr)
: m_data(new payload(std::forward<U>(x)))
{}
copy_on_write(const copy_on_write &x) noexcept
: m_data(x.m_data)
{
++m_data->count;
}
copy_on_write(copy_on_write && x) noexcept
: m_data(x.m_data)
{
x.m_data = nullptr;
}
~copy_on_write() {
if (m_data && (--m_data->count == 0)) {
delete m_data;
}
}
copy_on_write& operator=(const copy_on_write &x) noexcept {
if (&x != this) {
*this = copy_on_write(x);
}
return *this;
}
copy_on_write& operator=(copy_on_write && x) noexcept {
auto tmp = std::move(x);
swap(*this, tmp);
return *this;
}
element_type& write() {
if (!unique()) {
*this = copy_on_write(read());
}
return m_data->value;
}
const element_type& read() const noexcept {
return m_data->value;
}
friend inline void swap(copy_on_write &x, copy_on_write &y) noexcept {
using std::swap;
swap(x.m_data, y.m_data);
}
private:
bool unique() const noexcept {
return m_data->count == 1;
}
private:
payload *m_data;
};
/**
* Specializations for thread-safe code path
*/
template <typename T>
const T& cow_read(const T &v) {
return v;
}
template <typename T>
const T& cow_read(copy_on_write<T> &v) {
return v.read();
}
template <typename T>
T& cow_write(T &v) {
return v;
}
template <typename T>
T& cow_write(copy_on_write<T> &v) {
return v.write();
}
/**
* std::make_shared instantiates a lot a templates, and makes both compilation time
* and executable size far bigger than they need to be. We offer a make_shared
* equivalent that will avoid most instantiations with the following tradeoffs:
* - Not exception safe,
* - Allocates a separate control block, and will thus make the code slower.
*/
#ifdef SIGSLOT_REDUCE_COMPILE_TIME
template <typename B, typename D, typename ...Arg>
inline std::shared_ptr<B> make_shared(Arg && ... arg) {
return std::shared_ptr<B>(static_cast<B*>(new D(std::forward<Arg>(arg)...)));
}
#else
template <typename B, typename D, typename ...Arg>
inline std::shared_ptr<B> make_shared(Arg && ... arg) {
return std::static_pointer_cast<B>(std::make_shared<D>(std::forward<Arg>(arg)...));
}
#endif
/* slot_state holds slot type independent state, to be used to interact with
* slots indirectly through connection and scoped_connection objects.
*/
class slot_state {
public:
constexpr slot_state(group_id gid) noexcept
: m_index(0)
, m_group(gid)
, m_connected(true)
, m_blocked(false)
{}
virtual ~slot_state() = default;
virtual bool connected() const noexcept { return m_connected; }
bool disconnect() noexcept {
bool ret = m_connected.exchange(false);
if (ret) {
do_disconnect();
}
return ret;
}
bool blocked() const noexcept { return m_blocked.load(); }
void block() noexcept { m_blocked.store(true); }
void unblock() noexcept { m_blocked.store(false); }
protected:
virtual void do_disconnect() {}
auto index() const {
return m_index;
}
auto& index() {
return m_index;
}
group_id group() const {
return m_group;
}
private:
template <typename, typename...>
friend class ::sigslot::signal_base;
std::size_t m_index; // index into the array of slot pointers inside the signal
const group_id m_group; // slot group this slot belongs to
std::atomic<bool> m_connected;
std::atomic<bool> m_blocked;
};
} // namespace detail
/**
* connection_blocker is a RAII object that blocks a connection until destruction
*/
class connection_blocker {
public:
connection_blocker() = default;
~connection_blocker() noexcept { release(); }
connection_blocker(const connection_blocker &) = delete;
connection_blocker & operator=(const connection_blocker &) = delete;
connection_blocker(connection_blocker && o) noexcept
: m_state{std::move(o.m_state)}
{}
connection_blocker & operator=(connection_blocker && o) noexcept {
release();
m_state.swap(o.m_state);
return *this;
}
private:
friend class connection;
explicit connection_blocker(std::weak_ptr<detail::slot_state> s) noexcept
: m_state{std::move(s)}
{
if (auto d = m_state.lock()) {
d->block();
}
}
void release() noexcept {
if (auto d = m_state.lock()) {
d->unblock();
}
}
private:
std::weak_ptr<detail::slot_state> m_state;
};
/**
* A connection object allows interaction with an ongoing slot connection
*
* It allows common actions such as connection blocking and disconnection.
* Note that connection is not a RAII object, one does not need to hold one
* such object to keep the signal-slot connection alive.
*/
class connection {
public:
connection() = default;
virtual ~connection() = default;
connection(const connection &) noexcept = default;
connection & operator=(const connection &) noexcept = default;
connection(connection &&) noexcept = default;
connection & operator=(connection &&) noexcept = default;
bool valid() const noexcept {
return !m_state.expired();
}
bool connected() const noexcept {
const auto d = m_state.lock();
return d && d->connected();
}
bool disconnect() noexcept {
auto d = m_state.lock();
return d && d->disconnect();
}
bool blocked() const noexcept {
const auto d = m_state.lock();
return d && d->blocked();
}
void block() noexcept {
if (auto d = m_state.lock()) {
d->block();
}
}
void unblock() noexcept {
if (auto d = m_state.lock()) {
d->unblock();
}
}
connection_blocker blocker() const noexcept {
return connection_blocker{m_state};
}
protected:
template <typename, typename...> friend class signal_base;
explicit connection(std::weak_ptr<detail::slot_state> s) noexcept
: m_state{std::move(s)}
{}
protected:
std::weak_ptr<detail::slot_state> m_state;
};
/**
* scoped_connection is a RAII version of connection
* It disconnects the slot from the signal upon destruction.
*/
class scoped_connection final : public connection {
public:
scoped_connection() = default;
~scoped_connection() override {
disconnect();
}
/*implicit*/ scoped_connection(const connection &c) noexcept : connection(c) {}
/*implicit*/ scoped_connection(connection &&c) noexcept : connection(std::move(c)) {}
scoped_connection(const scoped_connection &) noexcept = delete;
scoped_connection & operator=(const scoped_connection &) noexcept = delete;
scoped_connection(scoped_connection && o) noexcept
: connection{std::move(o.m_state)}
{}
scoped_connection & operator=(scoped_connection && o) noexcept {
disconnect();
m_state.swap(o.m_state);
return *this;
}
private:
template <typename, typename...> friend class signal_base;
explicit scoped_connection(std::weak_ptr<detail::slot_state> s) noexcept
: connection{std::move(s)}
{}
};
/**
* Observer is a base class for intrusive lifetime tracking of objects.
*
* This is an alternative to trackable pointers, such as std::shared_ptr,
* and manual connection management by keeping connection objects in scope.
* Deriving from this class allows automatic disconnection of all the slots
* connected to any signal when an instance is destroyed.
*/
template <typename Lockable>
struct observer_base : private detail::observer_type {
virtual ~observer_base() = default;
protected:
/**
* Disconnect all signals connected to this object.
*
* To avoid invocation of slots on a semi-destructed instance, which may happen
* in multi-threaded contexts, derived classes should call this method in their
* destructor. This will ensure proper disconnection prior to the destruction.
*/
void disconnect_all() {
std::unique_lock<Lockable> _{m_mutex};
m_connections.clear();
}
private:
template <typename, typename ...>
friend class signal_base;
void add_connection(connection conn) {
std::unique_lock<Lockable> _{m_mutex};
m_connections.emplace_back(std::move(conn));
}
Lockable m_mutex;
std::vector<scoped_connection> m_connections;
};
/**
* Specialization of observer_base to be used in single threaded contexts.
*/
using observer_st = observer_base<detail::null_mutex>;
/**
* Specialization of observer_base to be used in multi-threaded contexts.
*/
using observer = observer_base<std::mutex>;
namespace detail {
// interface for cleanable objects, used to cleanup disconnected slots
struct cleanable {
virtual ~cleanable() = default;
virtual void clean(slot_state *) = 0;
};
template <typename...>
class slot_base;
template <typename... T>
using slot_ptr = std::shared_ptr<slot_base<T...>>;
/* A base class for slot objects. This base type only depends on slot argument
* types, it will be used as an element in an intrusive singly-linked list of
* slots, hence the public next member.
*/
template <typename... Args>
class slot_base : public slot_state {
public:
using base_types = trait::typelist<Args...>;
explicit slot_base(cleanable &c, group_id gid)
: slot_state(gid)
, cleaner(c)
{}
~slot_base() override = default;
// method effectively responsible for calling the "slot" function with
// supplied arguments whenever emission happens.
virtual void call_slot(Args...) = 0;
template <typename... U>
void operator()(U && ...u) {
if (slot_state::connected() && !slot_state::blocked()) {
call_slot(std::forward<U>(u)...);
}
}
// check if we are storing callable c
template <typename C>
bool has_callable(const C &c) const {
auto p = get_callable();
return eq_function_ptr(c, p);
}
template <typename C>
std::enable_if_t<function_traits<C>::must_check_object, bool>
has_full_callable(const C &c) const {
return has_callable(c) && check_class_type<std::decay_t<C>>();
}
template <typename C>
std::enable_if_t<!function_traits<C>::must_check_object, bool>
has_full_callable(const C &c) const {
return has_callable(c);
}
// check if we are storing object o
template <typename O>
bool has_object(const O &o) const {
return get_object() == get_object_ptr(o);
}
protected:
void do_disconnect() final {
cleaner.clean(this);
}
// retieve a pointer to the object embedded in the slot
virtual obj_ptr get_object() const noexcept {
return nullptr;
}
// retieve a pointer to the callable embedded in the slot
virtual func_ptr get_callable() const noexcept {
return get_function_ptr(nullptr);
}
#ifdef SIGSLOT_RTTI_ENABLED
// retieve a pointer to the callable embedded in the slot
virtual const std::type_info& get_callable_type() const noexcept {
return typeid(nullptr);
}
private:
template <typename U>
bool check_class_type() const {
return typeid(U) == get_callable_type();
}
#else
template <typename U>
bool check_class_type() const {
return false;
}
#endif
private:
cleanable &cleaner;
};
/*
* A slot object holds state information, and a callable to to be called
* whenever the function call operator of its slot_base base class is called.
*/
template <typename Func, typename... Args>
class slot final : public slot_base<Args...> {
public:
template <typename F, typename Gid>
constexpr slot(cleanable &c, F && f, Gid gid)
: slot_base<Args...>(c, gid)
, func{std::forward<F>(f)} {}
protected:
void call_slot(Args ...args) override {
func(args...);
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(func);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(func);
}
#endif
private:
std::decay_t<Func> func;
};
/*
* Variation of slot that prepends a connection object to the callable
*/
template <typename Func, typename... Args>
class slot_extended final : public slot_base<Args...> {
public:
template <typename F>
constexpr slot_extended(cleanable &c, F && f, group_id gid)
: slot_base<Args...>(c, gid)
, func{std::forward<F>(f)} {}
connection conn;
protected:
void call_slot(Args ...args) override {
func(conn, args...);
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(func);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(func);
}
#endif
private:
std::decay_t<Func> func;
};
/*
* A slot object holds state information, an object and a pointer over member
* function to be called whenever the function call operator of its slot_base
* base class is called.
*/
template <typename Pmf, typename Ptr, typename... Args>
class slot_pmf final : public slot_base<Args...> {
public:
template <typename F, typename P>
constexpr slot_pmf(cleanable &c, F && f, P && p, group_id gid)
: slot_base<Args...>(c, gid)
, pmf{std::forward<F>(f)}
, ptr{std::forward<P>(p)} {}
protected:
void call_slot(Args ...args) override {
((*ptr).*pmf)(args...);
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(pmf);
}
obj_ptr get_object() const noexcept override {
return get_object_ptr(ptr);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(pmf);
}
#endif
private:
std::decay_t<Pmf> pmf;
std::decay_t<Ptr> ptr;
};
/*
* Variation of slot that prepends a connection object to the callable
*/
template <typename Pmf, typename Ptr, typename... Args>
class slot_pmf_extended final : public slot_base<Args...> {
public:
template <typename F, typename P>
constexpr slot_pmf_extended(cleanable &c, F && f, P && p, group_id gid)
: slot_base<Args...>(c, gid)
, pmf{std::forward<F>(f)}
, ptr{std::forward<P>(p)} {}
connection conn;
protected:
void call_slot(Args ...args) override {
((*ptr).*pmf)(conn, args...);
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(pmf);
}
obj_ptr get_object() const noexcept override {
return get_object_ptr(ptr);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(pmf);
}
#endif
private:
std::decay_t<Pmf> pmf;
std::decay_t<Ptr> ptr;
};
/*
* An implementation of a slot that tracks the life of a supplied object
* through a weak pointer in order to automatically disconnect the slot
* on said object destruction.
*/
template <typename Func, typename WeakPtr, typename... Args>
class slot_tracked final : public slot_base<Args...> {
public:
template <typename F, typename P>
constexpr slot_tracked(cleanable &c, F && f, P && p, group_id gid)
: slot_base<Args...>(c, gid)
, func{std::forward<F>(f)}
, ptr{std::forward<P>(p)}
{}
bool connected() const noexcept override {
return !ptr.expired() && slot_state::connected();
}
protected:
void call_slot(Args ...args) override {
auto sp = ptr.lock();
if (!sp) {
slot_state::disconnect();
return;
}
if (slot_state::connected()) {
func(args...);
}
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(func);
}
obj_ptr get_object() const noexcept override {
return get_object_ptr(ptr);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(func);
}
#endif
private:
std::decay_t<Func> func;
std::decay_t<WeakPtr> ptr;
};
/*
* An implementation of a slot as a pointer over member function, that tracks
* the life of a supplied object through a weak pointer in order to automatically
* disconnect the slot on said object destruction.
*/
template <typename Pmf, typename WeakPtr, typename... Args>
class slot_pmf_tracked final : public slot_base<Args...> {
public:
template <typename F, typename P>
constexpr slot_pmf_tracked(cleanable &c, F && f, P && p, group_id gid)
: slot_base<Args...>(c, gid)
, pmf{std::forward<F>(f)}
, ptr{std::forward<P>(p)}
{}
bool connected() const noexcept override {
return !ptr.expired() && slot_state::connected();
}
protected:
void call_slot(Args ...args) override {
auto sp = ptr.lock();
if (!sp) {
slot_state::disconnect();
return;
}
if (slot_state::connected()) {
((*sp).*pmf)(args...);
}
}
func_ptr get_callable() const noexcept override {
return get_function_ptr(pmf);
}
obj_ptr get_object() const noexcept override {
return get_object_ptr(ptr);
}
#ifdef SIGSLOT_RTTI_ENABLED
const std::type_info& get_callable_type() const noexcept override {
return typeid(pmf);
}
#endif
private:
std::decay_t<Pmf> pmf;
std::decay_t<WeakPtr> ptr;
};
} // namespace detail
/**
* signal_base is an implementation of the observer pattern, through the use
* of an emitting object and slots that are connected to the signal and called
* with supplied arguments when a signal is emitted.
*
* signal_base is the general implementation, whose locking policy must be
* set in order to decide thread safety guarantees. signal and signal_st
* are partial specializations for multi-threaded and single-threaded use.
*
* It does not allow slots to return a value.
*
* Slot execution order can be constrained by assigning group ids to the slots.
* The execution order of slots in a same group is unspecified and should not be
* relied upon, however groups are executed in ascending group ids order. When
* the group id of a slot is not set, it is assigned to the group 0. Group ids
* can have any value in the range of signed 32 bit integers.
*
* @tparam Lockable a lock type to decide the lock policy
* @tparam T... the argument types of the emitting and slots functions.
*/
template <typename Lockable, typename... T>
class signal_base final : public detail::cleanable {
template <typename L>
using is_thread_safe = std::integral_constant<bool, !std::is_same<L, detail::null_mutex>::value>;
template <typename U, typename L>
using cow_type = std::conditional_t<is_thread_safe<L>::value,
detail::copy_on_write<U>, U>;
template <typename U, typename L>
using cow_copy_type = std::conditional_t<is_thread_safe<L>::value,
detail::copy_on_write<U>, const U&>;
using lock_type = std::unique_lock<Lockable>;
using slot_base = detail::slot_base<T...>;
using slot_ptr = detail::slot_ptr<T...>;
using slots_type = std::vector<slot_ptr>;
struct group_type { slots_type slts; group_id gid; };
using list_type = std::vector<group_type>; // kept ordered by ascending gid
public:
using arg_list = trait::typelist<T...>;
using ext_arg_list = trait::typelist<connection&, T...>;
signal_base() noexcept : m_block(false) {}
~signal_base() override {
disconnect_all();
}
signal_base(const signal_base&) = delete;
signal_base & operator=(const signal_base&) = delete;
signal_base(signal_base && o) /* not noexcept */
: m_block{o.m_block.load()}
{
lock_type lock(o.m_mutex);
using std::swap;
swap(m_slots, o.m_slots);
}
signal_base & operator=(signal_base && o) /* not noexcept */ {
lock_type lock1(m_mutex, std::defer_lock);
lock_type lock2(o.m_mutex, std::defer_lock);
std::lock(lock1, lock2);
using std::swap;
swap(m_slots, o.m_slots);
m_block.store(o.m_block.exchange(m_block.load()));
return *this;
}
/**
* Emit a signal
*
* Effect: All non blocked and connected slot functions will be called
* with supplied arguments.
* Safety: With proper locking (see pal::signal), emission can happen from
* multiple threads simultaneously. The guarantees only apply to the
* signal object, it does not cover thread safety of potentially
* shared state used in slot functions.
*
* @param a... arguments to emit
*/
template <typename... U>
void operator()(U && ...a) {
if (m_block) {
return;
}
// Reference to the slots to execute them out of the lock
// a copy may occur if another thread writes to it.
cow_copy_type<list_type, Lockable> ref = slots_reference();
for (const auto &group : detail::cow_read(ref)) {
for (const auto &s : group.slts) {
s->operator()(a...);
}
}
}
/**
* Connect a callable of compatible arguments
*
* Effect: Creates and stores a new slot responsible for executing the
* supplied callable for every subsequent signal emission.
* Safety: Thread-safety depends on locking policy.
*
* @param c a callable
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Callable>
std::enable_if_t<trait::is_callable_v<arg_list, Callable>, connection>
connect(Callable && c, group_id gid = 0) {
using slot_t = detail::slot<Callable, T...>;
auto s = make_slot<slot_t>(std::forward<Callable>(c), gid);
connection conn(s);
add_slot(std::move(s));
return conn;
}
/**
* Connect a callable with an additional connection argument
*
* The callable's first argument must be of type connection. This overload
* the callable to manage it's own connection through this argument.
*
* @param c a callable
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Callable>
std::enable_if_t<trait::is_callable_v<ext_arg_list, Callable>, connection>
connect_extended(Callable && c, group_id gid = 0) {
using slot_t = detail::slot_extended<Callable, T...>;
auto s = make_slot<slot_t>(std::forward<Callable>(c), gid);
connection conn(s);
std::static_pointer_cast<slot_t>(s)->conn = conn;
add_slot(std::move(s));
return conn;
}
/**
* Overload of connect for pointers over member functions derived from
* observer
*
* @param pmf a pointer over member function
* @param ptr an object pointer derived from observer
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Pmf, typename Ptr>
std::enable_if_t<trait::is_callable_v<arg_list, Pmf, Ptr> &&
trait::is_observer_v<Ptr>, connection>
connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) {
using slot_t = detail::slot_pmf<Pmf, Ptr, T...>;
auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
connection conn(s);
add_slot(std::move(s));
ptr->add_connection(conn);
return conn;
}
/**
* Overload of connect for pointers over member functions
*
* @param pmf a pointer over member function
* @param ptr an object pointer
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Pmf, typename Ptr>
std::enable_if_t<trait::is_callable_v<arg_list, Pmf, Ptr> &&
!trait::is_observer_v<Ptr> &&
!trait::is_weak_ptr_compatible_v<Ptr>, connection>
connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) {
using slot_t = detail::slot_pmf<Pmf, Ptr, T...>;
auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
connection conn(s);
add_slot(std::move(s));
return conn;
}
/**
* Overload of connect for pointer over member functions and
*
* @param pmf a pointer over member function
* @param ptr an object pointer
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Pmf, typename Ptr>
std::enable_if_t<trait::is_callable_v<ext_arg_list, Pmf, Ptr> &&
!trait::is_weak_ptr_compatible_v<Ptr>, connection>
connect_extended(Pmf && pmf, Ptr && ptr, group_id gid = 0) {
using slot_t = detail::slot_pmf_extended<Pmf, Ptr, T...>;
auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), std::forward<Ptr>(ptr), gid);
connection conn(s);
std::static_pointer_cast<slot_t>(s)->conn = conn;
add_slot(std::move(s));
return conn;
}
/**
* Overload of connect for lifetime object tracking and automatic disconnection
*
* Ptr must be convertible to an object following a loose form of weak pointer
* concept, by implementing the ADL-detected conversion function to_weak().
*
* This overload covers the case of a pointer over member function and a
* trackable pointer of that class.
*
* Note: only weak references are stored, a slot does not extend the lifetime
* of a suppied object.
*
* @param pmf a pointer over member function
* @param ptr a trackable object pointer
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Pmf, typename Ptr>
std::enable_if_t<!trait::is_callable_v<arg_list, Pmf> &&
trait::is_weak_ptr_compatible_v<Ptr>, connection>
connect(Pmf && pmf, Ptr && ptr, group_id gid = 0) {
using trait::to_weak;
auto w = to_weak(std::forward<Ptr>(ptr));
using slot_t = detail::slot_pmf_tracked<Pmf, decltype(w), T...>;
auto s = make_slot<slot_t>(std::forward<Pmf>(pmf), w, gid);
connection conn(s);
add_slot(std::move(s));
return conn;
}
/**
* Overload of connect for lifetime object tracking and automatic disconnection
*
* Trackable must be convertible to an object following a loose form of weak
* pointer concept, by implementing the ADL-detected conversion function to_weak().
*
* This overload covers the case of a standalone callable and unrelated trackable
* object.
*
* Note: only weak references are stored, a slot does not extend the lifetime
* of a suppied object.
*
* @param c a callable
* @param ptr a trackable object pointer
* @param gid an identifier that can be used to order slot execution
* @return a connection object that can be used to interact with the slot
*/
template <typename Callable, typename Trackable>
std::enable_if_t<trait::is_callable_v<arg_list, Callable> &&
trait::is_weak_ptr_compatible_v<Trackable>, connection>
connect(Callable && c, Trackable && ptr, group_id gid = 0) {
using trait::to_weak;
auto w = to_weak(std::forward<Trackable>(ptr));
using slot_t = detail::slot_tracked<Callable, decltype(w), T...>;
auto s = make_slot<slot_t>(std::forward<Callable>(c), w, gid);
connection conn(s);
add_slot(std::move(s));
return conn;
}
/**
* Creates a connection whose duration is tied to the return object
* Use the same semantics as connect
*/
template <typename... CallArgs>
scoped_connection connect_scoped(CallArgs && ...args) {
return connect(std::forward<CallArgs>(args)...);
}
/**
* Disconnect slots bound to a callable
*
* Effect: Disconnects all the slots bound to the callable in argument.
* Safety: Thread-safety depends on locking policy.
*
* If the callable is a free or static member function, this overload is always
* available. However, RTTI is needed for it to work for pointer to member
* functions, function objects or and (references to) lambdas, because the
* C++ spec does not mandate the pointers to member functions to be unique.
*
* @param c a callable
* @return the number of disconnected slots
*/
template <typename Callable>
std::enable_if_t<(trait::is_callable_v<arg_list, Callable> ||
trait::is_callable_v<ext_arg_list, Callable> ||
trait::is_pmf_v<Callable>) &&
detail::function_traits<Callable>::is_disconnectable, size_t>
disconnect(const Callable &c) {
return disconnect_if([&] (const auto &s) {
return s->has_full_callable(c);
});
}
/**
* Disconnect slots bound to this object
*
* Effect: Disconnects all the slots bound to the object or tracked object
* in argument.
* Safety: Thread-safety depends on locking policy.
*
* The object may be a pointer or trackable object.
*
* @param obj an object
* @return the number of disconnected slots
*/
template <typename Obj>
std::enable_if_t<!trait::is_callable_v<arg_list, Obj> &&
!trait::is_callable_v<ext_arg_list, Obj> &&
!trait::is_pmf_v<Obj>, size_t>
disconnect(const Obj &obj) {
return disconnect_if([&] (const auto &s) {
return s->has_object(obj);
});
}
/**
* Disconnect slots bound both to a callable and object
*
* Effect: Disconnects all the slots bound to the callable and object in argument.
* Safety: Thread-safety depends on locking policy.
*
* For naked pointers, the Callable is expected to be a pointer over member
* function. If obj is trackable, any kind of Callable can be used.
*
* @param c a callable
* @param obj an object
* @return the number of disconnected slots
*/
template <typename Callable, typename Obj>
size_t disconnect(const Callable &c, const Obj &obj) {
return disconnect_if([&] (const auto &s) {
return s->has_object(obj) && s->has_callable(c);
});
}
/**
* Disconnect slots in a particular group
*
* Effect: Disconnects all the slots in the group id in argument.
* Safety: Thread-safety depends on locking policy.
*
* @param gid a group id
* @return the number of disconnected slots
*/
size_t disconnect(group_id gid) {
lock_type lock(m_mutex);
for (auto &group : detail::cow_write(m_slots)) {
if (group.gid == gid) {
size_t count = group.slts.size();
group.slts.clear();
return count;
}
}
return 0;
}
/**
* Disconnects all the slots
* Safety: Thread safety depends on locking policy
*/
void disconnect_all() {
lock_type lock(m_mutex);
clear();
}
/**
* Blocks signal emission
* Safety: thread safe
*/
void block() noexcept {
m_block.store(true);
}
/**
* Unblocks signal emission
* Safety: thread safe
*/
void unblock() noexcept {
m_block.store(false);
}
/**
* Tests blocking state of signal emission
*/
bool blocked() const noexcept {
return m_block.load();
}
/**
* Get number of connected slots
* Safety: thread safe
*/
size_t slot_count() noexcept {
cow_copy_type<list_type, Lockable> ref = slots_reference();
size_t count = 0;
for (const auto &g : detail::cow_read(ref)) {
count += g.slts.size();
}
return count;
}
protected:
/**
* remove disconnected slots
*/
void clean(detail::slot_state *state) override {
lock_type lock(m_mutex);
const auto idx = state->index();
const auto gid = state->group();
// find the group
for (auto &group : detail::cow_write(m_slots)) {
if (group.gid == gid) {
auto &slts = group.slts;
// ensure we have the right slot, in case of concurrent cleaning
if (idx < slts.size() && slts[idx] && slts[idx].get() == state) {
std::swap(slts[idx], slts.back());
slts[idx]->index() = idx;
slts.pop_back();
}
return;
}
}
}
private:
// used to get a reference to the slots for reading
inline cow_copy_type<list_type, Lockable> slots_reference() {
lock_type lock(m_mutex);
return m_slots;
}
// create a new slot
template <typename Slot, typename... A>
inline auto make_slot(A && ...a) {
return detail::make_shared<slot_base, Slot>(*this, std::forward<A>(a)...);
}
// add the slot to the list of slots of the right group
void add_slot(slot_ptr &&s) {
const group_id gid = s->group();
lock_type lock(m_mutex);
auto &groups = detail::cow_write(m_slots);
// find the group
auto it = groups.begin();
while (it != groups.end() && it->gid < gid) {
it++;
}
// create a new group if necessary
if (it == groups.end() || it->gid != gid) {
it = groups.insert(it, {{}, gid});
}
// add the slot
s->index() = it->slts.size();
it->slts.push_back(std::move(s));
}
// disconnect a slot if a condition occurs
template <typename Cond>
size_t disconnect_if(Cond && cond) {
lock_type lock(m_mutex);
auto &groups = detail::cow_write(m_slots);
size_t count = 0;
for (auto &group : groups) {
auto &slts = group.slts;
size_t i = 0;
while (i < slts.size()) {
if (cond(slts[i])) {
std::swap(slts[i], slts.back());
slts[i]->index() = i;
slts.pop_back();
++count;
} else {
++i;
}
}
}
return count;
}
// to be called under lock: remove all the slots
void clear() {
detail::cow_write(m_slots).clear();
}
private:
Lockable m_mutex;
cow_type<list_type, Lockable> m_slots;
std::atomic<bool> m_block;
};
/**
* Specialization of signal_base to be used in single threaded contexts.
* Slot connection, disconnection and signal emission are not thread-safe.
* The performance improvement over the thread-safe variant is not impressive,
* so this is not very useful.
*/
template <typename... T>
using signal_st = signal_base<detail::null_mutex, T...>;
/**
* Specialization of signal_base to be used in multi-threaded contexts.
* Slot connection, disconnection and signal emission are thread-safe.
*
* Recursive signal emission and emission cycles are supported too.
*/
template <typename... T>
using signal = signal_base<std::mutex, T...>;
} // namespace sigslot
简单使用
#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"; };
// declare a signal instance with no arguments
sigslot::signal<> sig;
// sigslot::signal will connect to any callable provided it has compatible
// arguments. Here are diverse examples
sig.connect(f);
sig.connect(&s::m, &d);
sig.connect(&s::sm);
sig.connect(o());
sig.connect(lambda);
// Avoid hitting bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=68071
// on old GCC compilers
#ifndef __clang__
#if GCC_VERSION > 70300
auto gen_lambda = [](auto && ... /*a*/) { std::cout << "generic lambda\n"; };
sig.connect(gen_lambda);
#endif
#endif
// emit a signal
sig();
return 0;
}
带参数的使用
#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
void bar(float 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()() {}
};
// A generic function object that deals with any input argument
struct printer {
template <typename T, typename... Ts>
void operator()(T a, Ts && ...args) const {
std::cout << a;
(void)std::initializer_list<int>{
((void)(std::cout << " " << std::forward<Ts>(args)), 1)...
};
std::cout << "\n";
}
};
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 lambda_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(lambda_printer);
sig.connect(obj());
float f = 1.f;
short i = 2;
std::string s = "0";
// emit a signal
sig(f, i, false, s);
sig(f, i, true, s);
return 0;
}
使用示例
以下是一个简单的 sigslot
示例,展示了如何使用信号和槽机制:
#include <iostream>
#include <sigslot/signal.hpp>
class Button {
public:
sigslot::signal<> clicked;
};
class Dialog {
public:
void handleButtonClick() {
std::cout << "Button clicked!" << std::endl;
}
};
int main() {
Button button;
Dialog dialog;
// Connect the button's clicked signal to the dialog's handleButtonClick slot
button.clicked.connect(&dialog, &Dialog::handleButtonClick);
// Simulate button click
button.clicked();
return 0;
}
在这个示例中,Button
类有一个 clicked
信号,Dialog
类有一个 handleButtonClick
槽。通过 button.clicked.connect(&dialog, &Dialog::handleButtonClick)
,将按钮的 clicked
信号连接到对话框的 handleButtonClick
槽。当 button.clicked()
被调用时,handleButtonClick
槽会被自动调用。
总结
sigslot
是一个轻量级且易于使用的信号和槽库,适用于需要简单信号和槽机制的项目。虽然它的功能相对简单,但对于许多应用场景来说已经足够。如果你正在寻找一个轻量级的解决方案,sigslot
是一个值得考虑的选择。
附关于QT的元对象系统
元对象系统(Meta-Object System)是Qt框架中的一个核心组件,它提供了一种机制来支持运行时类型信息(RTTI,Runtime Type Information)和动态交互。元对象系统使得Qt能够在程序运行时获取对象的类型信息,并允许对象之间的动态通信,这包括但不限于信号与槽机制。
元对象系统的主要特点包括:
-
类型信息:元对象系统为每个Qt对象提供了类型信息,这使得程序能够在运行时识别对象的类类型。
-
对象构建:Qt使用元对象系统来创建对象。这包括对象的构造函数调用和内存分配。
-
信号与槽:元对象系统是信号与槽机制的基础。它允许Qt在运行时动态地连接信号和槽,即使它们在不同的线程中也是如此。
-
属性系统:Qt的属性系统允许开发者定义对象的属性,并在运行时读取和修改这些属性。元对象系统提供了这些属性的注册和管理。
-
枚举器和方法:元对象系统支持枚举器和方法的动态调用。这意味着可以在运行时查询对象支持的枚举类型和方法,并调用这些方法。
-
动态属性:元对象系统支持动态属性的概念,允许在运行时添加、修改或删除属性。
-
复制和克隆:元对象系统提供了对象复制和克隆的支持,这在Qt的模型/视图编程中非常有用。
-
多态性:元对象系统支持多态性,允许通过基类指针或引用调用派生类的方法。
-
事件处理:元对象系统在事件处理中也起着关键作用,它允许对象接收和处理不同类型的事件。
-
插件系统:Qt的插件系统依赖于元对象系统来动态加载和卸载插件。
元对象系统是Qt框架中非常强大的一个功能,它为Qt的许多高级特性提供了支持,包括但不限于信号与槽、属性系统、事件处理等。通过元对象系统,Qt能够实现高度的灵活性和动态性,使得开发者能够编写出更加强大和灵活的应用程序。
QT的信号与槽机制原理
Qt的信号与槽机制的实现原理是基于元对象系统(Meta-Object System, MOS)实现的。
Qt中的信号与槽机制是Qt框架的核心特性之一,它提供了一种灵活、高效的事件通信机制,使得各个组件之间能够进行松耦合的通信,从而实现模块化、可维护性强的程序设计。这种机制基于事件驱动的编程模型,通过信号和槽之间的连接,实现了对象之间的通信。在Qt中,信号和槽都是特殊的成员函数,它们通过特定的宏来声明和定义。信号使用signals
关键字声明,槽使用slots
关键字声明,而且它们可以是任意的成员函数。
- 元对象系统(MOC):每个继承自
QObject
的类都会通过元对象编译器(MOC)进行处理。MOC会在编译时生成一个针对该类的元对象,其中包含了该类的元信息,如类名、父类信息、信号列表、槽列表等。 - 信号和槽的声明与连接:在类的定义中,通过
signals
和slots
关键字声明信号和槽函数。使用QObject::connect()
函数建立信号与槽之间的连接时,编译器会在背后调用元对象系统的相关函数,将信号和槽的指针信息保存到一个连接表中。 - 信号的发射与槽函数的调用:当信号源对象发射信号时,实际上是调用了一个由MOC自动生成的
emit_signal()
函数,并传递了相应的参数。在这个函数内部,会根据连接表找到与该信号相关联的槽函数,并依次调用这些槽函数。当信号发射时,与之连接的槽函数会被自动调用,并传递相应的参数。这些槽函数被视为普通的成员函数,因此可以直接通过函数指针进行调用。
通过元对象系统,Qt可以在运行时实现信号和槽之间的连接和调用,从而实现了信号槽机制的功能。这种机制在处理用户界面事件、实现回调机制等方面非常有效,极大地增强了代码的灵活性和可维护性。
其他资源
https://zhuanlan.zhihu.com/p/652880307
sigslot库--一个简单的C++消息框架-CSDN博客
深入剖析WebRTC事件机制之Sigslot-腾讯云开发者社区-腾讯云
https://zhuanlan.zhihu.com/p/615949772
Qt 信号与槽机制原理_qt信号与槽机制原理-CSDN博客