在之前,我曾花三篇文章講述了C++委托機(jī)制的封裝,到最后可以實(shí)現(xiàn)任意類型函數(shù)的捆綁,包括lambda表達(dá)式的注冊。然而,這個委托機(jī)制還有一點(diǎn)需要完善,可能看標(biāo)題大家知道這個需要完善的點(diǎn)兒是什么,不過不知道也關(guān)系,這個文章會由淺入深的講述這個問題的來源,以及解決方案。
在直接上委托代碼之前我們先抽出問題本質(zhì),用簡單的代碼來發(fā)現(xiàn)問題,例子來源于<C++ PRimer>P612: 這個將編寫一個Agent函數(shù),它負(fù)責(zé)接受一個函數(shù)和兩個參數(shù),并且用這個函數(shù)來調(diào)用這兩個參數(shù)。
template<typename Function,typename Param1,typename Param2>void Agent(Function f,Param1 p1,Param2 p2){ f(p1,p2);}這個函數(shù)就相當(dāng)于委托的一個雛形,一般情況下,這個函數(shù)能工作得很好,然而在當(dāng)f的實(shí)例是一個接受引用參數(shù)的函數(shù)就會出現(xiàn)問題,因?yàn)闊o論左右值在模板的類型推演的過程中是不會帶引用的。例如:
template<typename T>void f(T a){}f(1) -> T為intint a;f(a) -> T為int如何解決這個問題呢?這里有兩種方案:
1.使用move()或者ref()來”提醒”模板推演成引用類型。
class A{public: A() { cout << "構(gòu)造" << 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;}以上方法雖然在這種情況下能夠解決問題,然而在實(shí)際過程中還是會有許多局限性:
在參數(shù)轉(zhuǎn)發(fā)路徑上始終要記住保持參數(shù)類型。也就是如果轉(zhuǎn)發(fā)層數(shù)過多,那么參數(shù)類型在中途可能還是會有部分修飾丟失。調(diào)用形式始終要和函數(shù)參數(shù)類型保持一致,不利于維護(hù)。那么,我們就比較希望能夠有一種方式能夠使得參數(shù)在轉(zhuǎn)發(fā)的路途中始終保持自身的完整類型。而這個方法就是forward.
在講forward之前需要引入一個新的概念——引用的折疊。 我們知道在C++語法中規(guī)定不能定義引用的引用。然而,可能有部分同學(xué)寫過如下代碼:
typedef int& rint;int a = 5;rint b = a;rint& c = b;而且,這段代碼是編譯通過的,可能有人就會說,經(jīng)過換名之后 rint&就是引用的引用。 然而實(shí)際上對于編譯器而言rint& 實(shí)質(zhì)就是 int& & <——請注意,兩個&之間有空格。 根據(jù)不同的換名可能會有以下幾種情況: - T& & - T&& & - T& && - T&& &&
然而對于這些情況,編譯器會發(fā)生引用折疊,如下:
| 原型 | 折疊后 |
|---|---|
| T& & | T& |
| T&& & | T& |
| T& && | T& |
| T&& && | T&& |
所以說利用模板推演和引用折疊可以找到參數(shù)的真正類型。
首先我們來看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是有兩個版本的,下面給出上面英文的注解的翻譯:
將一個左值實(shí)參按照其真實(shí)類型(左值或者右值)轉(zhuǎn)發(fā)(轉(zhuǎn)換)。將一個右值按照右值類型轉(zhuǎn)發(fā)(轉(zhuǎn)換)。由于篇幅原因我就只剖析第一個重載版本。剖析之前,我還是先要介紹forward的使用。 雖然forward是模板函數(shù),參數(shù)可以自動推演,然而forward的參數(shù)是一個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。這個模板是標(biāo)準(zhǔn)庫提供的標(biāo)準(zhǔn)類型轉(zhuǎn)換模板,為了方便閱讀,我將其余的一些貼在了文章的最末。 根據(jù)上一段講述的引用折疊我們知道_Ty&&折疊后就是_Ty的真正類型。 所以forward就是將左值實(shí)參按照給定的類型T進(jìn)行強(qiáng)轉(zhuǎn)。
了解了forward實(shí)現(xiàn)之后,我們來改寫上面的Agent函數(shù):
template<typename Function,typename Param1,typename Param2>void Agent(Function f,Param1&& p1,Param2&& p2){ f(forward<Param1>(p1),forward<Param2>(p2));}這樣,當(dāng)我將左值int傳入第一個參數(shù)的時(shí)候,Param1會根據(jù)引用折疊原理自動推演為int&而在調(diào)用f的過程中使用forward< Param1 >(p1)將p1保持Param1類型。
根據(jù)這個我改寫了委托的代碼,如下(在這里使用了模板參數(shù)包的展開技巧):
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)...); //注意:這里使用了模板參數(shù)包的展開技巧 }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 或者函數(shù) | 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 |
新聞熱點(diǎn)
疑難解答