標籤:c++ 模板 templates 高效c++ typename
- 條款41瞭解隱式介面和編譯期多態
- 條款42瞭解typename的雙重意義
條款41:瞭解隱式介面和編譯期多態
物件導向編程總是以顯示介面(explicit interfaces)和運行期多態(runtime polymorphism)來解決問題。例如
class Widget{ public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); …… }; void doProcessing(Widget& w) { if(w.size()>10 && w!=someNasyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
可以這樣說doProcessing內的w
- w的類型被聲明為Widget,所以w必須支援Widget介面。
- Widget的某些成員函數是virtual,w對於這樣函數的調用將表現出運行期多態(runtime polymorphism)。
在Templates和泛型程式設計的世界中,顯示介面和運行期多態仍然存在,但是更要到的是隱式介面(implicit interface)和編譯器多態(compile-time polymorphism)。將doProcessing從函數變為函數模板(function template)
template<typename T> void doProcessing(T& w) { if(w.size()>10 && w!=someNasyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
現在再來看doProcessing內的w
- w必須支援哪種介面,有template中執行於w身上的操作來決定。
- 凡涉及w的任何函數調用,例如operator>和operator!=,有可能造成template的具現化(instantiated),使這些調用得以成功。這樣的局現化發生在編譯期。以不同template參數具現化function template會導致調用不同的函數,這就是編譯期多態(compile-time polymorphism)。
編譯期多態和運行期多態不難區分,現在來看顯式介面和隱式介面的區別。通常顯式介面有函數的簽名式(函數名稱、參數類型、傳回值)構成。
class Widget{ public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); …… };
public介面由一個建構函式、一個解構函式、函數size,normalize、swap以及其參數、傳回值、常量性(constness)構成,還包括編譯器產生的copy建構函式和copy assignment操作符。
隱式介面就完全不同,它不是由函數簽名決定,而是由有效運算式(valid expression)組成。
template<typename T> void doProcessing(T& w) { if(w.size()>10 && w!=someNasyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
可以看出T(w類型)的隱式介面好像有這些約束
- 必須提供一個名為size的函數,該函數返回一個整數值
- 必須支援一個operator!=汗還是,用來比較兩個對象。
其實並不是必須滿足這兩個約束。T必須支援size成員函數,但是這個函數可能從base class繼承。這個函數不需要返回一個整數值,甚至不需要返回一個數實值型別。甚至不需要返回一個定義有operator>的類型。它唯一要做的就是返回一個類型為X的對象,而X對象加上一個int(10的類型)必須能夠調用一個operator>。這個operator>不需要非得取得一個類型為X的參數,它可以取得類型為Y的參數,只要存在一個隱式轉換能夠將類型X的對象轉換為類型為Y的對象。同理T不需要支援operator!=。
以上分析還沒有考慮operator&&被重載,一個串連詞的改變或許完全不同的某種東西,可能改變上述運算式的意義。
第一次以此種方式思考隱式介面會感覺不習慣。隱式介面僅僅由一組有效運算式構成,這個運算式可能看起來很複雜,但它們要求的約束條件一般而言相當直接又明確,例如:
if(w.size()>10 && w!=someNasyWidget)
關於函數size、operator>、operator&&、operator!=身上的約束條件,很難再說太多;但整體確認運算式約束條件很容易。if運算式必須為布爾值,因此整體運算式必須與bool相容。這時template doProcessing中型別參數T隱式介面的一部分,doProcessing要求其他隱式介面:copy建構函式、normalize和swap也必須對T型對象有效。
加諸於template參數身上的隱式介面和加諸於class對象身上介面一樣真實,都是在編譯期完成檢查,如果template中使用不支援template所要求的隱式介面,代碼不能編譯通過
總結
- class和template都支援介面和多態。
- class的介面是顯式的,以函數簽名為中心,多態是通過virtual函數發生於運行期。
- template的介面是隱式的,基於有效運算式。多態是通過template具體化和函數重載解析(function overloading resolution)發生在編譯期。
條款42:瞭解typename的雙重意義
使用模板時,可以用typename,也可以用class
template<class T> class Widget; template<typename T> class Widget;
兩者沒有什麼不同。作為template的型別參數,意義完全相同。在使用習慣上來說,很多人喜歡使用typename,因為這暗示參數並非一定要是個class類型。
C++有時不會把class和typename等價。有時候一定要用typename;為了瞭解原理,先來談談在template內指涉(refer to)的兩種名稱。
用一個例子來說明,現在有個template function,接收STL相容容器為參數,容器內對象可被賦值為int,這個函數列印第二個元素的值
template<typename C> void print2nd(const C& container) { if(container.size()>=2) { C::const_iterator iter(container.begin()); ++iter; int valaue=*iter; std::cout<<value; } }
現在解釋一下兩個local變數iterate和value。iterate類型為C::const_iterator,實際是什麼取悅於template參數C。template內出現的名稱如果依賴於某個template參數,稱這個名稱為從屬名稱(dependent names)。如果從屬名稱在class內呈嵌套狀,稱之為嵌套從屬名稱(nested dependent name)。C::const_iterator就是這樣的嵌套從屬名稱(nested dependent name),且指涉某類型。value是int類型,不依賴template參數,稱之為非從屬名稱(non-dependent names)。
嵌套從屬名稱有可能導致解析(parsing)困難,例如在print2dn這樣做
template<typename C>void print2nd(const C& container){ C::const_iterator* x; ……}
表面上看是聲明了一個local變數x,x類型是只需C::const_iterator的指標,這是因為我們直到C::const_iterator是個類型。如果不是這樣呢?例如,C內有個static成員變數碰巧命名為const_iterator,或x是個global變數;這樣的話就是一個相乘動作:const::const_iterator乘以x。編寫C++解析器的人必須操心這樣的輸入。
在直到C是什麼之前,不能確定C::cont_iterator是否為一個類型。當編譯器解析template print2nd時,不知道C是什麼東西。C++有個規則可以解析這個歧義狀態:如果解析器在template中遭遇到一個嵌套從屬名稱,它便假設這個明白不是個類型,除非你告訴它是。所以預設情況下,嵌套從屬名稱不是類型。這個規則還有個例外,稍後再提。
再來看看print2nd,C::const_iterator iter只有在C::const_iterator是個類型時才合理,但是C++預設認為它不是。要矯正這個形式,我們必須告訴C++C::const_iterator是個類型,在其前面放置關鍵字typename即可
template<typename C> void print2nd(const C& container) { if(container.size()>=2) { typename C::const_iterator iter(container.begin()); ++iter; int valaue=*iter; std::cout<<value; } }
“typename必須作為嵌套從屬類型名稱的首碼詞”這一規則的例外是:typename不可以出現在base class list內的嵌套從屬類型名稱之前,也不可以在member initialization list(成員初值列)中作為base class修飾符。
template<typename T> class Derived: public Base<T>::Nested{ //base class list中不允許typename public: explicit Derived(int x) :Base<T>::Nested(x) //mem init.list中不允許typename { typename Base<T>::Nested temp; //嵌套從屬名稱類型 …… //既不在base class list中,也不再mem init list中,作為base class修飾符要加上typename } …… };
再來看一個typename例子。一個function template接受一個迭代器,我們打算為該迭代器指涉的對象做一份local複件(副本)temp
template<typename IterT> void workWithIterator(IterT iter) { typename::std::iterator_traits<IterT>::value_type temp(*iter); …… }
typename::std::iterator_traits<IterT>::value_type是標準traits class(**條款**47)的一種運用。std::iterator_traits<IterT>::value_type是個嵌套從屬類名稱,所以在其前面放置typename。如果使用std::iterator_traits<IterT>::value_type感覺太長不習慣,可以考慮建立一個typedef,對於traits成員名稱,普遍的習慣是設定typedef名稱代表某個traits成員名稱。
template<typename IterT> void workWithIterator(IterT iter) { typedef typename::std::iterator_traits<IterT>::value_type value_type; value_type temp(*iter); …… }
最後提一下,typename相關規則在不同編譯器上有不同的實踐。這意味著typename和嵌套從屬名稱之間的互動,也許會在移植性方面帶來頭疼的問題。
總結
- 聲明template參數時,首碼關鍵字class和typename可以互換。
- 使用關鍵字typename標識嵌套從屬類型名稱;但不得在base class list(基類列)或member initialization list(成員初值列)內以它作為base class修飾符。
《Effective C++》:條款41-條款42