熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

Pure C++:泛型編程:模板特殊化

2022-06-13   來源: .NET編程 

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

  圖 顯示了 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^ s String^ s ) { return ( s>CompareTo( s ) < ) ? true :false; }

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

  即使類的類型 tStack<String^> 是從常規類模板定義(即由編譯器內部生成的專用於 String 的實例其中每個 elemType 占位符都被替換為 String 類型)實例化的類型 tStack<String^> 的每個對象都會調用特殊化的成員函數 mintStack::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 使用局部特殊化使其能夠很好地處理大小為 KB 的緩沖區

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

  Buffer 的局部特殊化只有一個類型參數 elemType因為大小的值固定為 局部模板特殊化的參數列表只列出了模板參數仍然未知的參數但是當您定義該模板的實例時必須同時指定這兩個參數(這與對一個參數使用默認值的情形不同)在下面的示例中局部類模板特殊化是用 elemType 為 String 的類型參數實例化的

  Buffer<String^> mumble;

  但是如果您改為下面的代碼行則編譯器會生成錯誤並將聲明標記為缺少第二個參數

  Buffer<String^> mumble; // 錯誤

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

  template <class elemType> ref class Buffer<elemType> {}; template <class elemType> ref class Buffer<elemType> {};

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

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

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

  局部特殊化的定義完全不同於常規模板的定義局部特殊化可以擁有一組與常規類模板完全不同的成員局部類模板特殊化的成員函數靜態數據成員和嵌套類型必須有自己的定義這與類模板特殊化相同類模板成員的常規定義決不能用於實例化局部類模板特殊化的成員

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

  函數模板特殊化

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

  template <class T> T max( T t T t ) { return ( t > t ? t :t ); }

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

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

  // 沒問題從參數類型推斷出 T 綁定到 String template<> String^ max( String^ String^ ); 引入此顯式特殊化後下面的調用就會解析為這個特殊化的實例 void foo( String^ s String^ s ) { String^ maxString = max( s s ); // }

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

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

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

  有一種情況省略函數模板特殊化的 template<> 部分不是錯誤在您聲明的普通函數帶有與模板實例化相匹配的返回類型和參數列表的情況下

  // 常規模板定義 template <class T> T max( T t T t ) { /* */ } // 沒問題普通函數聲明! String^ max( String^ String^ );

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

  void foo( String^ s String^ s ) { // 能否解析特殊化的實例? String^ maxString = max( muffy s ); // }

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

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

  如下所示的調用

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

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

  String^ maxString = max( muffy s ); // 錯誤

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

  template <class T> T max( T t T t );

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

  template <class Tclass T> ??? max( T t T t );

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

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

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

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

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

  max( );

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

  max( muffy s ); max( s muffy );

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

  max( s s );

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

  重載函數的解析過程

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

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

  只有在模板參數推斷成功時函數模板實例化才會進入候選函數集但是如果模板參數推斷失敗不會出現錯誤函數實例化沒有添加到候選函數集中

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

  因此此調用有兩個候選函數特殊化的模板實例化和非模板實例

  // 候選函數 // 特殊化的模板 template<> String^ max<String^>( String^ s String^ s ); // 非模板實例 String^ max( String^ String^ );

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

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

  實際上調用是明確的將調用非模板 max因為它優先於模板實例化原因是在某種程度上顯式實現的函數比通過常規模板創建的實例更為實用

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

  // 支持 String 的最終重載集 template <class T> T max( T t T t ) { /* */ } String^ max( String^ String^ );

  這不一定會很復雜但有一點是肯定的 在語言集成和靈活性方面它遠遠地超過了公共語言運行時 (CLR) 泛型功能可以支持的范圍

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

  請將您的疑問和意見通過 p 發送給 Stanley

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

  轉到原英文頁面


From:http://tw.wingwit.com/Article/program/net/201311/12528.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.