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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM