發新話題

[分享] C++中的函數模板

C++中的函數模板

<引言>  
函數模板是C++中的高級概念,初級用戶即使使用了一些標準庫中的函數模板都未必意識到。  作為強類型語言的C++,很多時候往往制約了一些較強語言功能的發揮,比如在Perl/PHP等腳本語言中,類型的模糊為實現普遍的算法  (比如min/max)提供了較好支持,同樣的算法可以施加在各種不同數據類型上,而講這些差別交給解釋器完成,無疑給用戶提供了很大的  方便。 在C中一般只能通過使用宏達到類似的效果,但顯然這不是個類型安全和穩妥的方法。而C++就必須為各種類型編寫函數(比如用  重載函數),更好的方法便是使用函數模板.

<定義>  
template <temp_par1,temp_par2,...>  temp_par是模板類型參數(template type parameter).它代表了一種類型,也可以是模板非類型參數(template nontype parameter),  它代表一個常量表達式.  template type parameter由class或typename後加一個標識符構成,在函數的模板參數表中這兩個關鍵字的意義相同它們表示後面的  參數名代表一個潛在的內置或用戶定義的類型,模板參數名由程序員選擇譬如  template <class Glorp>  Glorp min( Glorp a, Glorp b ) {       return a < b ? a : b;  }  template nontype parameter則在模板實例化期間是常量 如同非模板函數一樣函數模板也可以被聲明為inline 或extern 應該把指示符放在模板參數表後面而不是在關鍵字template 前面,如:  template <class Glorp>  inline  Glorp min( Glorp a, Glorp b ) {       return a < b ? a : b;  }

<關於typename和class>  
為了分析模板定義,編譯器必須能夠區分出碰到的表達式是一個計算表達式還是聲明類型的表達式.對於編譯器來說它並不是件容易的事情。  例如如果編譯器在模板定義中遇到表達式Parm::name, 且Parm 這個模板類型參數代表了一個類,那麼name 引用的一定是  Parm 的一個類型成員嗎template <class Parm, class U>arm minus( Parm* array, U value ){Parm::name * p; // 這是一個指針聲明還是乘法乘法 }  顯然,編譯器不知道name 是否為一個類型,因為它只有在模板被實例化之後才能找到Parm 表示的類的定義.為了讓編譯器能夠分析模板  定義,用戶必須指示編譯器哪些表達式是類型表達式.告訴編譯器一個表達式是類型表達式的機制是在表達式前加上關鍵字typename.  例如:如果我們想讓函數模板minus()的表達式Parm::name 是個類型名,因而使整個表達式是一個指針聲明我們應如下修改template <class Parm, class U>arm minus( Parm* array, U value ){typename Parm::name * p; // ok: 指針聲明 }這其實就是C++的關鍵字typename之所以出現的最初原因,現在則主要用在模板參數表中以指示一個模板參數是一個類型.

<實例化>  
函數模板在它被調用或取其地址時被實例化

<模板實參推演 template arguments deducation>  
在模板實參推演期間決定模板實參的類型時,編譯器不考慮函數模板實例的返回類型  模板實參推演的通用算法如下  
1 依次檢查每個函數實參,以確定在每個函數參數的類型中出現的模板參數  
2 如果找到模板參數,則通過檢查函數實參的類型推演出相應的模板實參  
3 函數參數類型和函數實參類型不必完全匹配.下列類型轉換可以被應用在函數實參上,以便將其轉換成相應的函數參數的類型     
   A.  左值轉換(數組到指針,函數到指針)     
   B.  限定修飾轉換(const,volatile)     
   C.  從派生類到基類類型的轉換.  
4 如果在多個函數參數中找到同一個模板參數,則從每個相應函數實參推演出的模板實參必須相同  從這裡可以看出,模板實參的推演並不允許有序標準轉換,因為這是在確定實例化哪個函數,而不是確定調用哪個函數

<顯式模板實參(explicitly specify)>  
比如: sum<char,unsigned int,int>(a,b,c)  此時,就沒有必要推演模板實參了,函數參數的類型已經固定。當函數模板實參被顯式指定時,把函數實參轉換成相應函數參數的類型可以應用任何隱式類型轉換(當然包括有序標準轉換)  顯式模板實參應該只被用在完全需要它們來解決二義性,或在模板實參不能被推演出來的上下文中時

<模板編譯模式>  
包括Inclusion Model和Separation Model,  對於前者,每個模板被實例化的文件中包含模板定義,並且定義通常放在頭文件中,就和inline函數的定義一樣。缺點是,沒有把實現和聲明分離開,並且可能導致同樣的代碼被多次編譯,同時造成多次實例化(這可以通過顯示實例化來解決).  對於後者,模板函數實現放在單獨的實現文件中,頭文件只是模板函數的聲明。但模板函數實現的定義需要出現export關鍵字。這樣的好處是分開了接口和實現,缺點是需要仔細規劃代碼,盡量防止在多個文件中出現export同一個函數模板,這有可能造成鏈接錯誤,另外一個遺憾是,目前很少有編譯器支持Separation Model(也許gcc或者VC7已經支持了)

<顯式實例化聲明>  
為了是模板更加實用,標準C++提供這個機制幫助用戶自行確定函數實例化的時機,用法如下:  
template <typename Type>   Type sum( Type op1, int op2 ) { /* ... */ }   // 顯式實例化聲明    template int* sum< int* >( int*, int );  
則int*的函數被顯式實例化.對於給定的函數模板實例,顯式實例化聲明在一個程序中只能出現一次,在顯式實例化聲明所在的文件中函數模板的定義必須被給出.如果該定義不可見則該顯式實例化聲明是錯誤的.  顯式實例化聲明是與另外一個編譯選項聯合使用的.該選項壓制了程序中模板的隱式實例化. 選項的名稱隨著編譯器不同而不同.(感覺這個機制並不是很有用,不知道gcc的選項是什麼)

<模板顯式特化(explicit specialization definition)>  
雖然模板已經為我們做了很多有益的工作,但有的時候,我們不希望這個模板函數實例化我們所有的類型,通常是因為有些類型如果例外處理反而對性能和功能有更大的好處。 比如說,對一個比較相等函數來說,對於普通類型(int,bool),它們的操作基本上是bitwise的比較,而對於C風格的字符串類型(char*),我們需要的則是memberwise的比較。因此C++提供模板顯示特化機制,給我們一個「重載」的機會。
例如:
// 通用的模板定義 template <class T> T max( T t1, T t2 ) {   return (t1 > t2 ? t1 : t2); } // const char* 顯式特化: // 覆蓋了來自通用模板定義的實例 typedef const char *PCC; template<> PCC max< PCC >( PCC s1, PCC s2 ) {    return ( strcmp( s1, s2 ) > 0 ? s1 : s2 ); }

注意: 在源文件中使用函數模板顯式特化之前必須先進行聲明 通常, 模板顯式特化的聲明被包含在每個用需要被特化的類型實參調用函數模板的文件中. 顯式特化的聲明應該被放在頭文件中,並在所有使用函數模板的程序中包含這個文件

<重載和函數模板>  
函數模板同樣可以被重載,只是名字解析由於模板實參推演過程的加入而變得稍許複雜了。主要的步驟如下:  
1.  編譯器看到一個函數調用點時,先確定所有同名的調用點的函數。除了普通的重載函數,候選函數集中還要再加上那些實參推演成功的模板函數,此時如果存在模板特化,則實際上該模板特化成為一個候選函數。  
2.  此時確定可選函數集(根據3種標準轉換),這一步和普通的重載解析一致。  
3.  對各種轉換進行分級打分,最後剩下的函數中,如果同時有普通函數和模板函數入選,則只取普通函數,如果普通函數不止一個,則編譯報錯。顯然,如果最後剩下的函數中沒有模板函數,那麼結果和一般的重載函數解析一致,即如果不止一個函數滿足要求,編譯報錯。  這裡有一個問題可能會引起困惑。
為什麼同時有普通函數和模板函數入選時要優先考慮普通函數。 這是為了應付這種情況,當我們不得不對某種類型採取顯式模板實參調用模板函數時,為了不修改整個文件中的多處函數調用,就直接用一個普通函數來聲明,而在普通函數里一次性完成顯示模板實參調用。
如以下所示:
// 函數模板定義 template <class Type> Type min( Type t1, Type t2 ) { ... } // 普通函數 int min( int a1, int a2 ) {   min<int>( a1, a2 ); } int main() { // 調用普通函數    min( ai[0], ss ); }  設想,如果在源程序很多地方都需要調用min<int>(int,int),
這不失為一個比較好的利用語言規則的措施,因為即使不利用這樣的規則,對程序員來說使用流行的編輯器軟件完全替換所有函數調用點也絕對是不難的事情。

<模板定義中的名字解析>  
在模板定義中,有些名字符號是需要到模板實例化時才能解析的,它們一般是模板的實參類型。而有些則是在模板定義時就可以解析的。前者通常稱為依賴於模板參數的名字(depend on a template parameter),而後者顯然是不依賴於模板參數的。  對於不依賴於模板參數的函數調用,必須在定義模板前事先聲明。  而對於依賴模板參數的函數調用,則必須在模板實例化前聲明。  因此,顯而易見的推論是,不依賴模板參數的名字都由模板庫的提供者聲明,而用戶只在需要實例化時給出名字的聲明即可。

<名字空間和函數模板>  
實際上很簡單,把函數模板定義在一個名字空間裡,就和把一個普通函數定義在名字空間裡一樣,使用前,或者using一下需要的函數,或者把整個名字空間一起using進來。  這兩者實際上沒有本質的關聯。

[ 本帖最後由 蔡逸竹 於 2007-5-14 03:05 編輯 ]

TOP

發新話題

本站所有圖文均屬網友發表,僅代表作者的觀點與本站無關,如有侵權請通知版主會盡快刪除。