• 使用 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 的特性。

  • 相关阅读:
    TTL与RS-485电平转换芯片MAX485/MAX3485
    RS485芯片介绍及典型应用电路
    脉冲电能表的组成及脉冲装置工作原理
    django-redis的安装及使用
    Python折线图——机器人UPtime Trend Chart
    ASP.Net DropDownList控件的使用方法
    C# ASP.Net数据库连接(Oracle)
    django根据已有数据库表生成model类
    Python Outlook发送邮件
    oracle将excel数据导入数据库
  • 原文地址:https://www.cnblogs.com/ForEmail5/p/7147782.html
Copyright © 2020-2023  润新知