Pure C++: 泛型程式設計,模板特殊化

來源:互聯網
上載者:User
 

 Pure C++:泛型程式設計:模板特殊化發布日期: 9/30/2005 | 更新日期: 9/30/2005

Stanley B. Lippman

在上一期專欄中我已經談到過,執行的操作不僅包括簡單儲存和檢索操作的參數化型別僅限於可安全綁定到它的可接受類型 [請參閱 Pure C++: CLR Generics Versus C++ Templates(英文)]。使用泛型,可以通過 where 子句顯式加上這些限制。在 C++/CLI 模板工具中,通過將函數模板或類模板(單個成員函數或整個類)特殊化,通常可以避免這些限制。例如,將 min 函數添加到上一期專欄的 tStack 類中。通常,我會使用常規 min 演算法,但那種演算法僅在我作為程式員的時候有用,而對我撰寫有關模板特殊化的文章沒有協助。為了方便起見,圖 1 中重現了 tStack 類的模板定義。

圖 2 顯示了 min 的一種可能的實現方法。我將定義一個局部變數 min_val 來存放最小元素,並將它初始化為容器的第一個元素。然後定義兩個迭代程式,將每個元素與 min_val 進行比較,如果其值比 min_val 小則為 min_val 重新賦值。現在,您能看出隱含的限制嗎?如果能,則您會得到:

if ( *it < min_val )

通常,對於 min 函數,只有能夠使用內建小於 (<) 運算子的類型或本身具有 operator<() 執行個體的類型才能綁定到 elemType 的類型。如果某個類型沒有定義 operator<(),並嘗試對此類型的項的 tStack 調用 min,則在 min 中使用無效的比較子時將出現編譯時間錯誤。例如,System::String 類沒有小於 (<) 運算子(它使用 IComparable 的 CompareTo 方法)。因此,如果我嘗試對使用 String 執行個體化的 tStack 調用 min,則它在編譯時間就會出錯,因為該比較操作失敗了。

有一種解決方案我不會使用:定義全域運算子 operator<(),該運算子使用 CompareTo 來比較兩個 String 類型的值。然後,tStack<String^>::min() 會自動調用這些全域運算子:

bool operator<( String^ s1, String^ s2 ) {return ( s1->CompareTo( s2 ) < 0 ) ? true :false;}

請記住,目標是防止當使用者指定的型別參數為 String 時執行個體化 tStack::min 成員函數定義,而希望使用 CompareTo 方法來定義自己的 tStack<String^>::min 執行個體。您可以使用顯式模板特殊化定義為類模板執行個體化的成員提供特殊化的定義,來實現此目的。此定義指明了模板名稱、指定模板的參數、函數參數列表和函數主體。關鍵字模板的後面是小於 (<) 和大於 (>) 標記,然後是類成員特殊化的定義(請參閱圖 3)。

即使類的類型 tStack<String^> 是從常規類模板定義(即由編譯器內部產生的專用於 String 的執行個體,其中每個 elemType 預留位置都被替換為 String 類型)執行個體化的,類型 tStack<String^> 的每個對象都會調用特殊化的成員函數 min。tStack::min 成員函數定義既不會被擴充,也不會在 tStack<String^> 中使用。

在有些情況下,可能整個類模板定義都不適合某種類型。在這種情況下,程式員可以提供一種定義來特殊化整個類模板。程式員可以提供 tStack<String^> 的定義:

template <class elemType>ref class tStack;// 類模板特殊化template<> ref class tStack<String^> {public:tStack();String^ pop();void push( Stack^ et );    // ...};

只有在聲明了常規類模板後,才能定義顯式類模板特殊化。如果您提供完整的類模板特殊化,則必須定義與此特殊化關聯的每個成員函數或待用資料成員。類模板的常規成員定義決不能用於建立顯式特殊化的成員定義,也不會被交叉檢查。這是因為類模板特殊化的類成員集可能與常規模板的類成員集完全不同。

定義完全特殊化的類模板(如 tStack<String^>)的成員時,請勿在其定義前添加特殊的 template<> 標記,而應該通過顯式列出實際的類型來指明特殊化定義,如下所示:

// 定義類模板特殊化的// 成員函數 min()String^ tStack<String^>::min() { ... }

局部模板特殊化

如果類模板有多個模板參數,您可以針對一個或一組特定的參數化值或類型來特殊化類模板。也就是說,您可能希望提供一個模板,使其除了某些模板參數已被實際類型或實際值替換以外,其他均與常規模板匹配。使用局部模板特殊化就可以實現此目的。例如,假設存在下面的 Buffer 類模板:

template <class elemType, int size>ref class Buffer { ... };

下面說明如何對 Buffer 使用局部特殊化,使其能夠很好地處理大小為 1KB 的緩衝區:

// 類模板 Buffer 的局部特殊化template <class elemType>ref class Buffer<elemType,1024> {// 對 1KB 大小使用特殊演算法...};

Buffer 的局部特殊化只有一個型別參數 elemType,因為大小的值固定為 1024。局部模板特殊化的參數列表只列出了模板參數仍然未知的參數。但是,當您定義該模板的執行個體時,必須同時指定這兩個參數(這與對一個參數使用預設值的情形不同)。在下面的樣本中,局部類模板特殊化是用 elemType 為 String 的型別參數執行個體化的:

Buffer<String^,1024> mumble;

但是,如果您改為下面的程式碼,則編譯器會建置錯誤,並將聲明標記為缺少第二個參數:

Buffer<String^> mumble;  // 錯誤

為什麼會這樣呢?如果開發人員以後引入一組特殊化的 Buffer(如下所示),會出現什麼情況?

template <class elemType>ref class Buffer<elemType,4096> {};template <class elemType> ref class Buffer<elemType,512> {};

如果前面樣本的聲明中不要求使用第二個參數,編譯器就無法區分這幾種特殊化!

局部特殊化與其對應的完整常規模板同名,在本例中為 Buffer。這就帶來了一個有趣的問題。請注意,Buffer<String^,1024> 的執行個體化既可以通過類模板定義進行,也可以通過局部特殊化進行。那麼,為什麼會選擇局部特殊化來執行個體化該模板呢?一般的規則是:如果聲明了局部類模板特殊化,編譯器就會選擇最特殊化的模板定義進行執行個體化。只有在無法使用局部特殊化時,才會使用常規模板定義。

例如,當必須執行個體化 Buffer<String^,2048> 時,由於此執行個體化與任何一個局部模板特殊化都不匹配,因此會選擇常規模板定義。

局部特殊化的定義完全不同於常規模板的定義。局部特殊化可以擁有一組與常規類模板完全不同的成員。局部類模板特殊化的成員函數、待用資料成員和巢狀型別必須有自己的定義,這與類模板特殊化相同。類模板成員的常規定義決不能用於執行個體化局部類模板特殊化的成員。

類模板的局部模板特殊化構成了現代 C++ 用法中一些非常複雜的設計慣用語的基礎。如果您對此感興趣,可以閱讀 Andrei Alexandrescu 撰寫的《Modern C++ Design: Generic Programming and Design Patterns Applied》(Addison-Wesley,2001 年版),瞭解此用法的詳細資料。

函數模板特殊化

非成員函數模板也可以進行特殊化。在有些情況下,您可以充分利用有關類型的一些專門知識,來編寫比從模板執行個體化的函數更高效的函數。在其他一些情況下,常規模板的定義對某種類型而言根本就是錯誤的。例如,假設您擁有函數模板 max 的定義:

template <class T>T max( T t1, T t2 ) {return ( t1 > t2 ? t1 :t2 );}

如果用 System::String 類型的模板參數執行個體化該函數模板,所產生的執行個體就無法編譯,因為正如您在前面所看到的,String 類不支援小於 (<) 或大於 (>) 運算子。圖 4 中的代碼說明了如何特殊化函數模板。(同樣必須先聲明常規函數模板才能進行特殊化。)

如果可以從函數參數推斷出模板參數,則可以從顯式特殊化聲明中對實際型別參數省略函數模板名稱的限定 max<String^>。例如,編譯器可以在下面的 max 模板特殊化中推斷出 T 綁定到 String,因此在這種情況下,為方便起見,該語言允許使用下面的簡寫標記法:

// 沒問題:從參數類型推斷出 T 綁定到 Stringtemplate<> String^ max( String^, String^ );引入此顯式特殊化後,下面的調用就會解析為這個特殊化的執行個體: void foo( String^ s1, String^ s2 ) {String^ maxString = max( s1, s2 );     // ...}

如果兩個參數的類型均為 String,常規函數模板不會擴充。這正是我們所需要的行為。

只要提供了顯式函數模板特殊化,就必須始終指定 template<> 和函數參數列表。例如,max 的下面兩個聲明不合法,並且在編譯時間會被標記為:

// 錯誤:無效的特殊化聲明// 缺少 template<>String^ max<String^>( String^, String^ );// 缺少函數參數列表template<> String^ max<String^>;

有一種情況,省略函數模板特殊化的 template<> 部分不是錯誤。即,在您聲明的普通函數帶有與模板執行個體化相匹配的傳回型別和參數列表的情況下:

// 常規模板定義template <class T>T max( T t1, T t2 ) { /* ... */ }// 沒問題:普通函式宣告!String^ max( String^, String^ );

毫無疑問,您經常會感到很無奈,並認為 C++ 真是太難理解了。您可能想知道,究竟為什麼所有人都希望聲明與模板執行個體化相匹配的普通函數,而不希望聲明顯式特殊化。那麼,請看下面的樣本,事情並不是完全按照您喜歡的方式進行的:

void foo( String^ s1, String^ s2 ) {// 能否解析特殊化的執行個體?String^ maxString = max( "muffy", s2 );     // ... }

在 C++/CLI 下,對於重載解決方案,字串文字的類型既是 const char[n] [其中 n 是文字的長度加一(用於終止Null 字元)],又是 System::String。這意味著,給定一組函數

void f( System::String^ );     // (1)void f( const char* );         // (2)void f( std::string );         // (3)

如下所示的調用

// 在 C++/CLI 下解析為 (1)f( "bud, not buddy" );

與 (1) 完全符合,而在 ISO-C++ 下,解析結果會是 (2)。因此,問題就是,對於函數模板的類型推斷而言,字串文字是否還是被當作 System::String 進行處理?簡言之,答案是“不”。(詳細的答案將是我下一期專欄的主題,該專欄將詳細介紹函數模板。)因此,不選擇 max 的特殊化 String 執行個體,下面對 max 的調用

String^ maxString = max( "muffy", s2 ); // 錯誤

在編譯時間會失敗,因為 max 的定義要求兩個參數的類型均為 T:

template <class T> T max( T t1, T t2 );

那您能做些什麼呢?像在下面的重新聲明中一樣,將模板改為帶有兩個參數的執行個體

template <class T1,class T2> ??? max( T1 t1, T2 t2 );

使我們能夠編譯帶有 muffy 和 s2 的 max 的調用,但會因大於 (>) 運算子而斷開;並且指定要返回的參數類型。

我想做的就是始終將字串文字強制轉換為 String 類型,這也是挽救普通函數的方法。

如果在推斷模板參數時使用了某個參數,那麼只有一組有限的類型轉換可用於將函數模板執行個體化的參數轉換為相應的函數參數類型。還有一種情況是顯式特殊化函數模板。正如您所看到的,從字串文字到 System::String 的轉換不屬於上述情況。

在存在有害字串文字的情況下,顯式特殊化無助於避免對類型轉換的限制。如果您希望不僅允許使用一組有限的類型轉換,則必須定義普通函數而不是函數模板特殊化。這就是 C++ 允許重載非模板函數和模板函數的原因。

我基本上已經講完了,不過還有最後一點需要說明。建立一組您在圖 5 中看到的 max 函數意味著什嗎?您知道調用

max( 10, 20 );

始終會解析為常規模板定義,並將 T 推斷為 int。同樣,您現在還知道調用

max( "muffy", s2 );max( s2, "muffy" );

始終會解析為普通函數執行個體(其中文字字串轉換為 System::String),但是有一個問題,調用

max( s2, s2 );

會解析為三個 max 函數中的哪一個?要回答此問題,我們要查看解析重載函數的過程。

重載函數的解析過程

解析重載函數的第一步是建立候選函數集。候選函數集包含與被調用的函數同名並且在調用時能夠看到其聲明的函數。

第一個可見函數是非模板執行個體。我將該函數添加到候選列表中。那麼函數模板呢?在能夠看到函數模板時,如果使用函數調用參數可以執行個體化函數,則該模板的執行個體化被視為候選函數。在我的樣本中,函數參數為 s2,其類型為 String。模板參數推斷將 String 綁定到 T,因此模板執行個體化 max(String^,String^) 將添加到候選函數集中。

只有在模板參數推斷成功時,函數模板執行個體化才會進入候選函數集。但是,如果模板參數推斷失敗,不會出現錯誤;即,函數執行個體化沒有添加到候選函數集中。

如果模板參數推斷成功,但是模板是為推斷出的模板參數顯式特殊化的(正如我的樣本一樣),會怎麼樣呢?結果是,顯式模板特殊化(而不是通過常規模板定義執行個體化的函數)將進入候選函數。

因此,此調用有兩個候選函數:特殊化的模板執行個體化和非模板執行個體。

// 候選函數// 特殊化的模板...template<> String^ max<String^>( String^ s1, String^ s2 );// 非模板執行個體String^ max( String^, String^ );

解析重載函數的下一步是從候選函數集中選擇可行函數集。對於要限定為可行函數的候選函數,必須存在類型轉換,將每個實際參數類型轉換為相應的形式參數類型。在該樣本中,兩個候選函數都是可行的。

解析重載函數的最後一步是,對參數所應用的類型轉換進行分級,以選擇最好的可行函數。例如,兩個函數看起來都很好。既然兩個函數都可行,那麼這是否應該被視為不明確的調用?

實際上,調用是明確的:將調用非模板 max,因為它優先於模板執行個體化。原因是,在某種程度上,顯式實現的函數比通過常規模板建立的執行個體更為實用。

令人吃驚的是,在解決有害字串文字的情況中,我已經徹底消除了調用以前的 String 特殊化的可能性,因此我可以消除這個問題。我只需要常規模板聲明以及重載的非模板執行個體:

// 支援 String 的最終重載集template <class T>T max( T t1, T t2 ) { /* ... */ }String^ max( String^, String^ );

這不一定會很複雜,但有一點是肯定的 - 在語言整合和靈活性方面,它遠遠地超過了通用語言執行平台 (CLR) 泛型功能可以支援的範圍。

模板特殊化是 C++ 範本設計的基礎。它提供了最好的效能,克服了對單個或系列類類型的限制,具有靈活的設計模式,並且在實際代碼中已證實其巨大價值。在下一期專欄中,我將深入分析 C++/CLI 對模板函數和常規函數的支援。

請將您的疑問和意見通過 purecpp@microsoft.com 發送給 Stanley。

Stanley B. Lippman 是 Microsoft 公司 Visual C++ 團隊的體繫結構設計師。他從 1984 年開始在 Bell 實驗室與 C++ 的設計者 Bjarne Stroustrup 一起研究 C++。此後,他在 Disney 和 DreamWorks 製作過動畫,還擔任過 JPL 的進階顧問和 Fantasia 2000 的軟體技術主管。

轉到原英文頁面



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.