概述
信号槽是 Qt 框架中用于对象间通信的一种方式。这种机制允许对象在发生特定事件时发出信号,而其他对象可以连接这些信号并提供相应的处理函数(槽),以响应这些事件。有一种熟悉的感觉对不对,其实类似于事件驱动编程,现在让我们看看如何实现。
核心概念和用途
- 事件驱动的通信:Qt 的信号和槽允许对象在发生特定事件时发出通知(信号),并由其他对象响应这些事件(槽)。
- 回调的注册与触发:在 Qt 中,你可以使用
connect
来注册槽函数,使用emit
来触发信号。 - 参数传递:Qt 的信号可以带有参数,这些参数会传递给槽函数。
- 动态行为:Qt 的信号和槽机制是动态的,可以在运行时连接和断开。
- 生命周期管理:Qt 信号和槽机制可以处理对象的生命周期,例如使用
QObject
的子类。我们可以通过std::weak_ptr
来处理可能的对象生命周期问题,防止回调尝试访问已经销毁的对象。 - 灵活性和通用性:Qt 的信号和槽非常灵活,可以连接任何可调用的对象。也提供了类似的灵活性,允许连接函数、lambda 表达式、bind 表达式等。
- 回调的执行策略:Qt 中的槽可以是排队的或直接的,可以根据需要选择。
用Cpp标准实现
函数成员
在此之前首先介绍一下封装的函数成员:
enum class CallbackResult {
Keep,
Erase,
};
#if __cpp_lib_move_only_function // standard feature-test macro
using Functor = std::move_only_function<void(T...)>;
#else
using Functor = std::function<CallbackResult(T...)>;
#endif
std::vector<Functor> m_callbacks;
这里的 Functor
类型有两个可能的定义:
- 如果支持
std::move_only_function
(C++23 引入):- 使用
std::move_only_function
作为Functor
类型。std::move_only_function
是 C++23 新引入的类型,用于存储只可移动的函数对象。它的优势在于,一旦被移动,源对象将不再可用,这有助于避免悬垂引用等问题。 - 这里的
std::move_only_function
模板使用void(T...)
作为参数,表示回调函数没有返回值,接受T...
类型的参数列表。
- 使用
- 如果不支持
std::move_only_function
:- 回退到使用
std::function
作为Functor
类型。std::function
是一个通用的多态函数包装器,它可以存储、调用和复制任何可调用对象,其签名与模板参数给定的签名匹配。 - 这里的
std::function
模板使用CallbackResult(T...)
作为参数,表示回调函数返回CallbackResult
类型,接受T...
类型的参数列表。
- 回退到使用
Functor
类型在 Signal
类模板中用于:
- 存储回调函数,允许
Signal
对象在emit
被调用时执行这些回调。 - 提供类型安全和灵活性,允许存储任何匹配签名的可调用对象。
Tag类
保证强类型安全。
inline constexpr struct once_flag_t {
explicit once_flag_t() = default;
} once_flag;
enum class nth_flag_t : size_t {};
connect函数
这里展示的是最终重载的版本,对于connect
函数,提供了灵活的方式来注册不同类型的回调函数,无论是独立的函数还是对象的成员函数,都可以很容易地连接到 Signal
对象上。
1. 接受可调用对象的 connect
函数模板
template<class Func>
void connect(Func cb) {
if constexpr (std::is_invocable_r_v<CallbackResult, Func, T...>) {
// 如果回调的返回类型是 CallbackResult,直接添加到回调列表
m_callbacks.push_back(std::move(cb));
} else if constexpr (std::is_invocable_r_v<bool, Func, T...>) {
// 如果回调的返回类型是 bool,包装回调使其返回 CallbackResult 类型
m_callbacks.push_back([ca = std::move(cb)](T&&...args) {
if (ca(std::forward<T>(args)...)) {
return CallbackResult::Erase;
}
return CallbackResult::Keep;
});
} else {
// 默认情况,假设回调不需要特殊处理返回值
m_callbacks.push_back([ca = std::move(cb)](T&&...args) {
ca(std::forward<T>(args)...);
return CallbackResult::Keep;
});
}
}
- 这个
connect
函数接受一个可调用对象cb
,例如函数、lambda 表达式或std::function
对象。 - 它使用
if constexpr
和std::is_invocable_r_v
来检查cb
的返回类型是否为CallbackResult
或bool
。- 如果返回类型是
CallbackResult
,直接将cb
移动到回调列表m_callbacks
中。 - 如果返回类型是
bool
,创建一个新的 lambda 表达式,它调用原始回调ca
并根据ca
的返回值转换为CallbackResult
类型。 - 如果回调的返回类型既不是
CallbackResult
也不是bool
,则假设回调不需要处理返回值,并创建一个新的 lambda 表达式,它调用原始回调并返回CallbackResult::Keep
。
- 如果返回类型是
2. 接受成员函数和对象的 connect
函数模板
template<class Self, class MemFn, class... Tag>
void connect(Self self, MemFn mem_fn, Tag... tag) {
m_callbacks.push_back(detail_::bind(std::move(self), mem_fn, tag...));
}
- 这个
connect
函数接受一个对象self
、一个成员函数指针mem_fn
和一些标签参数Tag...
。 - 它使用
detail_::bind
函数来创建一个绑定到成员函数的回调,并将这个回调添加到回调列表m_callbacks
中。 detail_::bind
根据传入的标签参数Tag...
创建不同类型的绑定,例如一次性执行或按次数执行的回调。
这两个 connect
函数模板提供了灵活的方式来注册不同类型的回调函数,无论是独立的函数还是对象的成员函数。
bind函数
bind可以说是整个函数的灵魂,它的作用是创建一个闭包(通常是 lambda 表达式),该闭包在被调用时,会调用与某个对象相关联的成员函数。这个 bind
函数有几个重载版本,用于不同的绑定场景,比如一次性调用、按次数调用,以及处理弱引用(std::weak_ptr
)的情况。
首先是关于weak_ptr的判断:
template<class Self>
std::shared_ptr<Self> lock_if_weak(std::weak_ptr<Self> const &self) {
return self.lock();
}
template<class Self>
Self const &lock_if_weak(Self const &self) {
return self;
}
1. 一次性调用的 bind
函数模板
template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn, once_flag_t) {
return [self = std::move(self), mem_fn](auto... args) {
auto const &ptr = lock_if_weak(self);
if (ptr == nullptr) return CallbackResult::Erase;
((*ptr).*mem_fn)(args...);
return CallbackResult::Erase;
};
}
- 这个版本接受一个
Self
类型的对象self
,一个成员函数指针MemFn
,以及一个once_flag_t
类型的标记。 - 创建的闭包在第一次调用时会执行成员函数,然后返回
CallbackResult::Erase
,表示这个回调应该从回调列表中删除。
2. 按次数调用的 bind
函数模板
template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn, nth_flag_t nth) {
return [self = std::move(self), mem_fn,
nth = static_cast<int>(nth)](auto... args) mutable {
if (nth <= 0) { return CallbackResult::Erase; }
auto const &ptr = lock_if_weak(self);
if (ptr == nullptr) return CallbackResult::Erase;
((*ptr).*mem_fn)(args...);
nth--;
if (nth <= 0) return CallbackResult::Erase;
return CallbackResult::Keep;
};
}
- 这个版本接受一个
Self
类型的对象self
,一个成员函数指针MemFn
,以及一个nth_flag_t
类型的参数,表示回调应该被调用的次数。 - 创建的闭包会根据
nth
参数的值来决定是否继续保留回调。每次调用时,nth
都会减一,当nth
减到 0 或以下时,回调会被删除。
3. 常规 bind
函数模板
template<class Self, class MemFn>
auto bind(Self self, MemFn mem_fn) {
return [self = std::move(self), mem_fn](auto... args) {
auto const &ptr = lock_if_weak(self);
if (ptr == nullptr) return CallbackResult::Erase;
((*ptr).*mem_fn)(args...);
return CallbackResult::Keep;
};
}
- 这是最基本的
bind
版本,接受一个Self
类型的对象self
和一个成员函数指针MemFn
。 - 创建的闭包在每次调用时都会执行成员函数,并返回
CallbackResult::Keep
,表示回调应该保留在回调列表中。
通用特性
- 所有
bind
函数模板都会创建一个 lambda 表达式,该表达式捕获self
和mem_fn
。 self
可能是一个std::weak_ptr
,lock_if_weak
函数用于尝试获取一个强引用(std::shared_ptr
)。- 如果
lock_if_weak
返回nullptr
,表示原始对象已经被销毁,回调应该被删除。 - 成员函数通过
((*ptr).*mem_fn)(args...)
语法调用,args...
是传递给闭包的参数,使用std::forward
来保持它们的值类别(左值或右值)。
bind
函数的目的是将成员函数与对象绑定,创建一个可以存储在 Signal
类中的回调。这允许 Signal
类的用户将成员函数作为回调连接到信号上,而不必担心对象的生命周期问题。
为什么按指捕获
按值捕获(value capture)意味着 lambda 表达式会复制捕获的变量到其闭包环境中。这与按引用捕获(reference capture)相对,后者仅捕获对原始变量的引用。
- 对象的生命周期管理
使用按值捕获对于对象的生命周期管理至关重要,尤其是当捕获 std::weak_ptr
时:
auto bind(Self self, MemFn mem_fn, nth_flag_t nth) {
return [self = std::move(self), mem_fn, nth](...) {
// ...
};
}
这里,self
是按值捕获的,这意味着 std::weak_ptr
被移动到 lambda 表达式中,并存储为一个局部变量。当 lambda 被调用时,它尝试通过 lock_if_weak
获取一个 std::shared_ptr
。如果 self
已经无效(即原始对象已经被销毁),则 lock
将返回 nullptr
,这时 lambda 可以决定不执行成员函数或执行一些清理工作。
- 避免悬空引用
按值捕获避免了悬空引用的问题。如果使用按引用捕获,lambda 将直接引用 self
,如果 self
在 lambda 调用之前被销毁,那么 lambda 中的引用将变成悬空引用,这可能导致未定义行为。
- 确保数据的独立性
按值捕获确保了每个 lambda 表达式实例拥有其捕获变量的独立副本。这在多线程环境中特别重要,因为它避免了因多个线程访问同一资源而导致的数据竞争问题。
- 可移动性
使用 std::move
与按值捕获结合使用,可以确保资源(如智能指针)的所有权在捕获时被转移给 lambda,而不是共享。这有助于防止资源的意外复制或多次释放。
- 模板参数的灵活性
在模板编程中,按值捕获提供了更大的灵活性,因为模板可以处理不同类型的参数,包括那些没有默认构造函数或不能被复制的类型。在下面这个示例中,通过按值捕获 std::shared_ptr
来避免悬空引用,并且 weakSelf
只在 lambda 内部有效。总之,按值捕获在 signal.h
中的 bind
函数模板中用于确保 lambda 表达式安全地处理可能具有复杂生命周期的对象,同时保持代码的简洁性和灵活性。
std::weak_ptr<SomeClass> weakPtr = ...;
auto callback = [weakSelf = weakPtr.lock()]() {
if (weakSelf) {
// 使用 weakSelf 做一些操作
}
};
模板函数省略的版本
// 避免重复声明多个版本
template<class Self, class MemFn>
auto bind_once(Self self, MemFn mem_fn) {
return [self = std::move(self), mem_fn](auto... args) {
((*self).*mem_fn)(args...);
return CallbackResult::Erase;
};
}
template<class Self, class MemFn>
auto bind_nth(Self self, MemFn mem_fn, size_t nth = 10) {
return [self = std::move(self), mem_fn, nth](auto... args) mutable {
if (nth <= 0) { return CallbackResult::Erase; }
((*self).*mem_fn)(args...);
nth--;
if (nth <= 0) return CallbackResult::Erase;
return CallbackResult::Keep;
};
}
template<class Self, class MemFn>
auto bind(std::weak_ptr<Self> self, MemFn mem_fn) {
return [self = std::move(self), mem_fn](auto... args) {
auto ptr = self.lock();
if (ptr == nullptr) { return CallbackResult::Erase; }
((*ptr).*mem_fn)(args...);
return CallbackResult::Keep;
};
}
template<class Self, class MemFn>
void connect_once(Self self, MemFn mem_fn) {
m_callbacks.push_back(bind_once(std::move(self), mem_fn));
}
template<class Self, class MemFn>
void connect_nth(Self self, MemFn mem_fn, size_t nth = 10) {
m_callbacks.push_back(bind_nth(std::move(self), mem_fn, nth));
}
template<class Self, class MemFn>
void connect_weak(std::shared_ptr<Self> self, MemFn mem_fn) {
m_callbacks.push_back(bind(std::weak_ptr<Self>(self), mem_fn));
}