使用 C++11 编写类似 QT 的信号槽——下篇
要实现 Signal-Slot,Signal 类中应该拥有一个保存 std::function 的数组:
template<class FuncType> class Signal { public: std::vector<std::function<FuncType>> functionals; };
接下来将会按照下图中可能出现的问题设计 Signal-Slot:
1、当对象 A 被摧毁时,funcA 应该自动从 vector 中移除。
要实现自动管理操作,最好的方式是使用 C++ 的智能指针进行管理。智能指针作为一个单纯的变量,当智能指针为对象一个成员变量时,它的生命周期和对象一样,在对象被摧毁的同时智能指针也会自动销毁掉。因此需要一个 SlotImpl 类对 std::function 进行管理:
template<class FuncType> class SlotImpl { public: ~SlotImpl() { // 从 signal 对象的数组中移除 } Signal* signal; std::function<FuncType> function; };
当 SlotImpl 被摧毁时,会调用析构函数将 function 从数组中移除。那么什么时候摧毁 SlotImpl 呢?前面已经说过,SlotImpl 的生命周期由智能指针来管理。使用一个类 Slot,封装该智能指针:
template<class FuncType> class Slot { public: std::shared_ptr<SlotImpl<FuncType>> slot; };
然后将 Slot 作为类 A 的成员属性:
class A { public: void funcA() {} Slot<void(void)> slot; };
所以,当对象 A 被摧毁时,Slot 对象被摧毁,由于智能指针的关系,SlotImpl 对象也会被销毁,最后 SlotImpl 的析构函数中 funcA 从数组中移除。
其中有一个小问题,就是对象 signalA 也应该拥有 SlotImpl。SlotImpl 以什么形式保存在 signalA 中时,才能确保 SlotImpl 的生命周期是由 Slot 的智能指针 std::share_ptr 管理,而不是 signalA?
signalA 要维护对象 SlotImpl 的指针(从而进行回调操作),但绝不允许指染对象的生命周期。根据上面这句话,应该想到智能指针 std::weak_ptr(weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放)。
Signal 类的设计更改为:
template<class FuncType> class Signal { public: std::vector<std::weak_ptr<SlotImpl<FuncType>>> slot; };
解决完第一个问题,还有第二个问题。
2、发生赋值操作 signalB = signalA 是,signalA 和 signalB 应该指向同一个数组。
为什么要指向同一个数组?因为当对象 A 被摧毁时,funcA 要从所有 Signal 对象中移除。显然赋值操作后,signalA 和 signalB 都拥有 funcA。如果 signalB 的数组只是 signalA 数组的拷贝,当 A 被摧毁后(因为保存类 signalA 的对象指针,很定会从 signalA 的数组中移除),signalB 发生回调操作时,会调用一个不存在的函数,最后报错。
解决的方法也是使用智能指针,和上面 Slot 的一样,使用类 SignalImpl:
template<class FuncType> class SignalImpl { public: std::weak_ptr<SlotImpl<FuncType>> slot; }; //------------------------------------------------------- template<class FuncType> class Signal { public: std::shared_ptr<SignalImpl<FuncType>> impl; };
使用 std::share_ptr,发生赋值操作后,signalA 和 signalB 都指向同一个 SignalImpl。
重点部分都已经介绍完,下面给出完整代码:
Signal.h
#pragma once #include <functional> #include <memory> #include <vector> namespace Simple2D { //--------------------------------------------------------------------- // bind_member //--------------------------------------------------------------------- template<class Return, class Type, class... Args> std::function<Return(Args...)> bind_member(Type* instance, Return(Type::*method)(Args...)) { /* 匿名函数 */ return[=] (Args&&... args) -> Return { /* 完美转发:能过将参数按原来的类型转发到另一个函数中 */ /* 通过完美转发将参数传递给被调用的函数 */ return (instance->*method)(std::forward<Args>(args)...); }; } //--------------------------------------------------------------------- // SignalImpl //--------------------------------------------------------------------- template<class SlotImplType> class SignalImpl { public: std::vector<std::weak_ptr<SlotImplType>> slots; }; //--------------------------------------------------------------------- // SlotImpl //--------------------------------------------------------------------- class SlotImpl { public: SlotImpl() {} virtual ~SlotImpl() {} /* 将该函数定义成已删除的函数,任何试图调用它的行为将产生编译期错误,是 C++11 标准的内容 */ SlotImpl(const SlotImpl&) = delete; /* 将该函数定义成已删除的函数,任何试图调用它的行为将产生编译期错误,是 C++11 标准的内容 */ SlotImpl& operator= (const SlotImpl&) = delete; }; //--------------------------------------------------------------------- // SlotImplT //--------------------------------------------------------------------- template<class FuncType> class SlotImplT : public SlotImpl { public: SlotImplT(const std::weak_ptr<SignalImpl<SlotImplT>>& signal, const std::function<FuncType>& callback) : signal(signal) , callback(callback) { } ~SlotImplT() { std::shared_ptr<SignalImpl<SlotImplT>> sig = signal.lock(); if ( sig == nullptr ) return; for ( auto it = sig->slots.begin(); it != sig->slots.end(); ++it ) { if ( it->expired() || it->lock().get() == this ) { it = sig->slots.erase(it); if ( it == sig->slots.end() ) { break; } } } } std::weak_ptr<SignalImpl<SlotImplT>> signal; std::function<FuncType> callback; }; //--------------------------------------------------------------------- // Slot //--------------------------------------------------------------------- class Slot { public: Slot() {} ~Slot() {} template<class T> explicit Slot(T impl) : impl(impl) {} operator bool() const { return static_cast< bool >(impl); } private: std::shared_ptr<SlotImpl> impl; }; //--------------------------------------------------------------------- // Signal //--------------------------------------------------------------------- template<class FuncType> class Signal { public: Signal() : impl(std::make_shared<SignalImpl<SlotImplT<FuncType>>>()) {} template<class... Args> void operator()(Args&&... args) { std::vector<std::weak_ptr<SlotImplT<FuncType>>> slotVector = impl->slots; for ( std::weak_ptr<SlotImplT<FuncType>>& weak_slot : slotVector ) { std::shared_ptr<SlotImplT<FuncType>> slot = weak_slot.lock(); if ( slot ) { slot->callback(std::forward<Args>(args)...); } } } Slot connect(const std::function<FuncType>& func) { std::shared_ptr<SlotImplT<FuncType>> slotImpl = std::make_shared<SlotImplT<FuncType>>(impl, func); /* 由于 SignalImpl 使用的是 std::weak_ptr,push_back 操作不会增加引用计数。 因此,如果调用函数 connect 后的返回值没有赋值给 Slot 对象,过了这个函数的 作用域 slotImpl 对象就会被释放掉 */ impl->slots.push_back(slotImpl); return Slot(slotImpl); } template<class InstanceType, class MemberFuncType> Slot connect(InstanceType instance, MemberFuncType func) { return connect(bind_member(instance, func)); } private: std::shared_ptr<SignalImpl<SlotImplT<FuncType>>> impl; }; }
在 Signal 类中对操作符 () 进行重载,使用了可变参模板,使用完美转发将参数传递到回调函数中。
下面有几点注意的问题:
1、SlotImplT 保存 SignalImpl 对象指针时使用了弱引用智能指针 std::weak_ptr,因为 SlotImplT 维护指针只是为了将 std::function 从 Signal 数组中移除,而不会指染 Signal 的生命周期。
2、要访问 std::weak_ptr 时,使用函数 lock 返回一个临时的 std::share_ptr。
总结:这个 Signal-Slot 是在 ClanLib 游戏引擎的源码中的,并非我原创。只是以如何编写 Signal-Slot 的思路对源码进行解析。虽然只有 100 多行代码,但其中包含了许多的 C++11 的特性。