了解 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,重點部分下篇文章再說。