在之前,我曾花三篇文章講述了C++委托機制的封裝,到最后可以實現任意類型函數的捆綁,包括lambda表達式的注冊。然而,這個委托機制還有一點需要完善,可能看標題大家知道這個需要完善的點兒是什么,不過不知道也關系,這個文章會由淺入深的講述這個問題的來源,以及解決方案。
在直接上委托代碼之前我們先抽出問題本質,用簡單的代碼來發現問題,例子來源于<C++ PRimer>P612: 這個將編寫一個Agent函數,它負責接受一個函數和兩個參數,并且用這個函數來調用這兩個參數。
template<typename Function,typename Param1,typename Param2>void Agent(Function f,Param1 p1,Param2 p2){ f(p1,p2);}這個函數就相當于委托的一個雛形,一般情況下,這個函數能工作得很好,然而在當f的實例是一個接受引用參數的函數就會出現問題,因為無論左右值在模板的類型推演的過程中是不會帶引用的。例如:
template<typename T>void f(T a){}f(1) -> T為intint a;f(a) -> T為int如何解決這個問題呢?這里有兩種方案:
1.使用move()或者ref()來”提醒”模板推演成引用類型。
class A{public: A() { cout << "構造" << endl; } A(const A&) { cout << "拷貝" << endl; } A(A&&) { cout << "移動" << endl; }};template<typename T>void X(T a) { }int main(){ A a; X(ref(a)); X(move(a)); return 0;}以上方法雖然在這種情況下能夠解決問題,然而在實際過程中還是會有許多局限性:
在參數轉發路徑上始終要記住保持參數類型。也就是如果轉發層數過多,那么參數類型在中途可能還是會有部分修飾丟失。調用形式始終要和函數參數類型保持一致,不利于維護。那么,我們就比較希望能夠有一種方式能夠使得參數在轉發的路途中始終保持自身的完整類型。而這個方法就是forward.
在講forward之前需要引入一個新的概念——引用的折疊。 我們知道在C++語法中規定不能定義引用的引用。然而,可能有部分同學寫過如下代碼:
typedef int& rint;int a = 5;rint b = a;rint& c = b;而且,這段代碼是編譯通過的,可能有人就會說,經過換名之后 rint&就是引用的引用。 然而實際上對于編譯器而言rint& 實質就是 int& & <——請注意,兩個&之間有空格。 根據不同的換名可能會有以下幾種情況: - T& & - T&& & - T& && - T&& &&
然而對于這些情況,編譯器會發生引用折疊,如下:
原型 | 折疊后 |
---|---|
T& & | T& |
T&& & | T& |
T& && | T& |
T&& && | T&& |
所以說利用模板推演和引用折疊可以找到參數的真正類型。
首先我們來看forward的源碼
template<class _Ty> inlineconstexpr _Ty&& forward( typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT{ // forward an lvalue as either an lvalue or an rvalue return (static_cast<_Ty&&>(_Arg));}template<class _Ty> inlineconstexpr _Ty&& forward( typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT{ // forward an rvalue as an rvalue static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call"); return (static_cast<_Ty&&>(_Arg));}可見forward是有兩個版本的,下面給出上面英文的注解的翻譯:
將一個左值實參按照其真實類型(左值或者右值)轉發(轉換)。將一個右值按照右值類型轉發(轉換)。由于篇幅原因我就只剖析第一個重載版本。剖析之前,我還是先要介紹forward的使用。 雖然forward是模板函數,參數可以自動推演,然而forward的參數是一個remove_reference萃取后的類型,也就是我們可以推演出remove_reference<_Ty>::type,但是無法推演出_Ty。 所以使用過程中,我們還是要顯式給出_Ty;例如: forward< T >(obj)。
所以_Ty就是我們提供的類型T。 至于remove_reference< T > 作用就是去掉類型T的所有引用修飾。也就是說假如T為Type&或者Type&&,那么remove_reference< T >::type就是Type。這個模板是標準庫提供的標準類型轉換模板,為了方便閱讀,我將其余的一些貼在了文章的最末。 根據上一段講述的引用折疊我們知道_Ty&&折疊后就是_Ty的真正類型。 所以forward就是將左值實參按照給定的類型T進行強轉。
了解了forward實現之后,我們來改寫上面的Agent函數:
template<typename Function,typename Param1,typename Param2>void Agent(Function f,Param1&& p1,Param2&& p2){ f(forward<Param1>(p1),forward<Param2>(p2));}這樣,當我將左值int傳入第一個參數的時候,Param1會根據引用折疊原理自動推演為int&而在調用f的過程中使用forward< Param1 >(p1)將p1保持Param1類型。
根據這個我改寫了委托的代碼,如下(在這里使用了模板參數包的展開技巧):
template<typename T>class Delegate{};template<typename Return, typename...Params>class Delegate<Return(Params...)>{public: typedef Return (*FunType) (Params...); Delegate(FunType fun) :_fun(fun) {} Return Operator () (Params... params) { return _fun(forward<Params>(params)...); //注意:這里使用了模板參數包的展開技巧 }private: FunType _fun;};void fun(int&& a) { cout << a << endl; }int main(){ Delegate<void(int&&)> a(fun); a(2); return 0;}名稱 | 若T為 | 則Mod< T >::type為 | 否則Mod< T >::type為 |
---|---|---|---|
remove_reference | X&或者X& | X | T |
add_const | X&、const X 或者函數 | T | const T |
add_lvalue_reference | X&或者X&& | X& | T& |
add_rvalue_reference | X&或者X&& | X&& | T&& |
remove_pointer | X* | X | T |
add_pointer | X&或者X&& | X* | T* |
make_signed | unsigned X | X | T |
make_unsigned | 帶符號類型 | unsigned X | T |
remove_extent | X[n] | X | T |
remove_all_extents | X[n1][n2]… | X | T |
新聞熱點
疑難解答