使用 C++11 編寫類似 QT 的信號槽——上篇


  了解 QT 的應該知道,QT 有一個信號槽 Singla-Slot 這樣的東西。信號槽是 QT 的核心機制,用來替代函數指針,將不相關的對象綁定在一起,實現對象間的通信。

  考慮為 Simple2D 添加一個類似的信號槽,實現對象間的通信。當然,功能比較簡單,不過對於 Simple2D 就足夠了。最終的使用看起來像是這樣的:

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

class B
{
public:
    void FuncB(int v1, float v2, std::string str)
    {
        log("B: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

 

    A objA;
    B objB;

    Signal<void(int, float, std::string)> signal;

    Slot slot1 = signal.connect(&objA, &A::FuncA);
    Slot slot2 = signal.connect(&objB, &B::FuncB);

    signal(10, 20, "Signal-Slot test");

  類 A 和 類 B 分別有一個函數(返回類型、參數個數及參數類型一樣),然后將 A 對象 objA 的 FuncA 函數和 B 對象 objB 的 FuncB 函數綁定到信號對象 signal 中,通過信號 signal 的調用,實現對 FuncA 和 FuncB 函數的調用。輸出窗口的輸出內容為:

 

  

  Signal-Slot 能夠實現對象間的解耦,接下來按照上面的代碼,用 C++11 的特性編寫信號槽。

 

  信號槽 Signal-Slot

  要實現上面的功能似乎並不困難,核心內容就是對回調函數的使用。

  將需要綁定的對象函數保存到 std::function 中,再把 std::function 保存到信號 Signal 對象中,使用數組保存 std::function 能夠實現一個 Signal 對應多個 Slot,最后重載 Signal 的操作符 ()。接下來將圍繞上面的步驟實現 Signal-Slot。

 

  std::function

  std::function(引入頭文件 <functional>) 是 C++11 的內容,通過 std::function 對 C++ 中各種可調用實體(普通函數、類成員函數、Lambda表達式、函數指針、以及其它函數對象等)的封裝,形成一個新的可調用的 std::function 對象。

  如果要將成員函數綁定到 std::function 對象中,可以通過以下的代碼實現:

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

  

    std::function<void(int, float, std::string)> Functional;

    A objA;
    Functional = std::bind(&A::FuncA, objA, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    Functional(20, 55, "functional test");

  輸出結果:

  通過 std::bind 函數類成員函數綁定到 std::function 中,但對於參數要使用占位符 std::placeholders::_x,由於 FuncA 函數有 3 個參數,所以要使用 3 個占位符。

  要實現 Signal-Slot,就要把任意的類成員函數綁定到 std::function 中。對於上面的情況,由於 FuncA 函數有 3 個參數,所以要使用 3 個占位符。對於那些不確定參數個數的類成員函數,如何把它們統一的綁定到 std::function 中呢?或許可以把參數個數為 1 - 10 的常用情況都列舉出來,但這樣並不是一個號方法。

 

  類成員函數的函數指針

  在解決將不確定參數個數的類成員函數綁定到 std::function 前,先看一看不用 std::function 實現的類成員函數的回調函數。

class A
{
public:
    void FuncA(int v1, float v2, std::string str)
    {
        log("A: --%d--%f--%s--", v1, v2, str.c_str());
    }
};

 

    typedef void(A::*Functionl)(int, float, std::string);

    A objA;

    Functionl functional = &A::FuncA;
    A* objAPtr = &objA;

    (objAPtr->*functional)(20, 55, "functional test");

  輸出結果:

  實現的方法和普通函數的函數指針類似,只不過定義函數指針的時候要使用類名 + ::,使用的時候也需要使用對象的指針(這意味着你要多保存一個對象指針)。在不使用 std::function 的情況下,實現類成員函數的回調函數要復雜的多。但有一個好處,就是綁定時和函數參數的個數無關。

  結合上面兩種方式的類成員函數的回調,就可以解決那個問題了——將不確定參數個數的類成員函數綁定到 std::function。

 

  bind_member 類成員綁定函數

  你應該要注意到,無論是 std:: function<void(int, float, std::string)> 的方式,還是 typedef void(Class::*Functional)(int, float, std::string) 的方式,都必須確定函數的返回類型和參數的類型(一旦 std::function 的函數格式確定了,就不能綁定其他格式的函數)。

  下面要編寫一個函數 bind_member,功能是將類成員函數(任意返回類型,任意參數類型,任意參數個數)綁定到 std::function 中。它看上去是這樣的:

    std::function<void(int, float, std::string)> Functional;

    A objA;
    Functional = bind_member(&objA, &A::FuncA);

    Functional(20, 55, "functional test");

  輸出結果:

  上面使用 bind_member 函數的代碼中,你可以看出兩種方式實現類成員函數回調的影子。那么如何實現 bind_member 呢?由於存在函數返回類型,所以要用到函數模板;由於函數的參數個數和參數類型不同,所以要用到可變參模板;如果你不了解可變參模板,可以看下面關於可變參模板的簡單介紹。

 

 

  可變參模板

  變參模板是 C++11 的新特性,其基本語法為:

template<class... Args>

  和普通模板不同,添加了三個點...,表示 Args 是模板參數包(template type parameter pack),是一連串任意的參數打成的一個包。下面舉一個例子(定義一個函數,接受任意參數並輸出)說明如何使用可變參模板:

        template<class... Args>
        void Log(Args... args)
        {
            printf("");
        }

  調用函數 Log 時,傳入 1、2、3、4 四個參數:

Log(1, 2, 3, 4);

  雖然定義了一個可變參模板的函數 Log,但內部如何實現才能輸出 1, 2, 3, 4  呢?也就是如何獲取參數包中的參數,如果能分別獲取參數包中的參數就能使用函數 printf 輸出了。

  這個是參數包的展開問題,可以使用遞歸函數的方法展開參數包。因此,需要兩個重載函數實現參數包的展開:

        template<class T, class... Args>
        void Log(T header, Args... args)
        {
            printf("--%d--\n", header);
            Log(args...);
        }

        void Log(int value)
        {
            printf("--%d--\n", value);
        }

  第二個函數可以理解,但是第一個函數是什么意思?這個先不理它,看下面的函數調用:

        Log(1);                // 1
        Log(1, 2);             // 2
        Log(1, 2, 3);          // 3
        Log(1, 2, 3, 4);       // 4

   1、當傳入的參數只有 1 時,毫無疑問會調用第二個函數,將 1 輸出。

  2、當傳入的參數為 1 和 2 時,可以猜測它會調用第一個函數:1 給 header 變量,然后輸出。剩下的 2 給 args,由於 args 只有一個參數 2,所以接下來的 Log(args...) 會調用第二個函數輸出 2。參數包的展開結束。

  3、當傳入的參數為 1, 2, 3 時,顯然它會調用第一個函數:1 給 header 變量,然后輸出。剩下的 2, 3 給 args,那么接下來的 Log(args...) 調用的是哪一個函數呢?(第一感覺是 args... 表示着一個變量,應該調用第二個函數才對,因為第二個函數接收一個參數,但這樣就不能展開接下來的 2 和 3 了)如果能理解這一步,就能理解如何展開參數包了。答案是 args... 會被拆成兩部分,第一個參數 2 為一部分,剩下的 3 作為另一部分。既然分成了兩部分,它會調用第一個函數處理(第一次接觸變參模板的人,很容易把第一個函數 Log 理解成結束兩個參數的函數,但並不是)。 接下來的展開和步驟 2 的只有參數 1 和 2 時一樣,所以遞歸展開參數包結束。

  4、當傳入的參數為 1, 2, 3, 4 時,這次用圖片來說明:

 

  bind_member 實現

  結合以上的內容,你可以實現 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)...);
        };
    }

 

  代碼中只是利用了可變參模板的參數包,解決了函數參數類型和參數個數不確定的問題。然后將函數指針的調用封裝在一個匿名函數中,再綁定到 std::function 中。其中使用了 C++11 的完美轉發,上面也做了簡單的介紹。

 

  避免文章過長,分成兩部分來實現 Signal-Slot,重點部分下篇文章再說。


免責聲明!

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



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