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

posted @ 2017-07-11 14:30  为了邮箱5  阅读(2465)  评论(1编辑  收藏  举报