以前在學校C++模板用的比較少,碰到的問題也就少。
而在工作中模板的使用隨處可見,在遇到問題中學習,也就對模板有了新的認識和理解。
下面是一個簡單的小結。
首先這一點是需要最先明確的,之前就是沒有理解這一點,所以對模板的認識一直停留在表明。
我們借助以下例子來理解這一個點:
templateclass AutoList { public: AutoList() {} ~AutoList() {} bool getAutoList() {return true;} private: T value; };
上面我們定義了一個類模板,但是它不是類——AutoList並不是一個類類型,而是一個類模板。
因此你如果寫出以下代碼,編譯器將會拒絕:
int main() { AutoList myList; return 0; }
In function ‘int main()’: 錯誤:missing template arguments before ‘myList’ 錯誤:expected ‘;’ before ‘myList'
你必須為AutoList提供一個模板參數,使它實例化成為一個類型:
int main() { AutoListintList; AutoList longList; return 0; }
上面我們分別傳入了兩個參數int和long,於是類模板將通過實例化產生了兩個獨立的類型。
你可以理解為,上面代碼經過編譯器處理之後,變成了如下代碼:
class AutoList{ public: AutoList() {} ~AutoList() {} bool getAutoList() {return true;} private: int value; }; class AutoList { public: AutoList() {} ~AutoList() {} bool getAutoList() {return true;} private: long value; }; int main() { AutoList intList; AutoList longList; return 0; }
舉個簡單的例子:你在做蛋糕的時候,類模板AutoList就好比提供給編譯器的一個模子,而實例化後的AutoList< int >就好比蛋糕類型。
模子它本身並不屬於蛋糕類型,你需要把各種做蛋糕的材料填到模子裡面去,才能得到一個實實在在的蛋糕,而這些蛋糕,根據你加入的材料不同,會有不同的口味——草莓味的、巧克力味的、牛奶味的——這就是實例化的過程。
也許以上例子不是特別恰當,但是還是非常直觀的。
回到我們的例子,編譯器在編譯以上代碼的過程中,碰到AutoList< int >,於是用int去實例化類模板AutoList,得到一個類型AutoList< int >,然後又碰到AutoList< long >,於是用long去實例化類模板AutoList,得到一個類型AutoList< long >,兩個類AutoList< int >和AutoList< long >各有一份代碼。之後如果又遇到AutoList< int >,因為已經有一份該類的定義代碼了,所以直接用就可以了。在編譯器編譯完成之後,類模板也就沒用了,說不定在最後生成的代碼中,類模板已經被丟棄,只剩下一個個根據模板實例化來的類類型。
總結一下:每次實例化,編譯器根據傳入的模板參數來實例化模板,生成一份新的代碼。
模板的這種實例化行為,帶來的一個問題就是——代碼膨脹。
一開始你以為只有一份代碼,可是事實上,你實例化了多少個類型,就有多少份類似的代碼——只是會用具體的參數來替換掉T。
// ok templateinline T test(const T& value); // error inline template T test(const T& value);
一般情況下,模板實參推斷的過程中不會進行隱式類型轉換來匹配已有的實例,而是會生成新的實例。
看以下兩個例子:
templatebool test(const T& l, const T& r) { return true; } int main() { short l = 128; int r = 1024; test(l, r); //error:此處將試圖實例化test(shor, int),而模板中兩個參數必須是同一個類型 return 0; }
我們做一下修改:
templatebool test(const L& l, const R& r) { return true; } int main() { short l_short = 128; int l_int = 128; int r = 1024; test(l_int, r); //實例化了test(const int&, const int&) test(l_short, r); // 實例化了test(const short&, const int&) return 0; }
在上面的第一個例子中,最容易出錯的就是下面這種情況:
int a[10], b[12]; test(a, b);
你以為a和b都是int類型的數組,然而結果確出乎你的意料:
錯誤:對‘test(int [10], int [12])’的調用沒有匹配的函數
我們知道,數組作為實參時,實際上會發生類型退化——int類型的數組將退化為指針int *。然而當我們為這個參數加上引用時,退化將不會發生,此時傳遞的不再是一個指針,而是整個數組,別忘了,數組是有大小的——也就是 int[10]和int[12]實際上是兩種不同的類型!
關於const轉換:
1.接受const**引用**或const指針的函數可以分別使用非const對象的引用或指針來調用,無需產生新的實例化——將非const傳遞給const是安全的。
2.如果函數接受非引用類型,形參類型和實參都忽略const——對於pass by value來說,const實參傳遞給非const形參並不會對實參造成任何影響。
如果你已經理解了模板本身不是類或函數,那麼這個知識點對你來說是很好理解的。
有了以上的基礎,我們再來聊一聊模板編譯模型。
模板編譯模型有兩種:包含編譯模型 和 分別編譯模型。
在包含編譯模型中:
1.只有在看到用到模板時編譯器才產生特定的模板實例。
2.要進行實例化,編譯器必須能夠訪問定義模板的源代碼。
第一點很好理解,沒有往模子到倒任何材料,編譯器永遠不知道會生產出什麼味道的蛋糕。
而第二點可以這樣理解:你說你有個蛋糕模子,但是卻不把模子給我,我無法給你生產你要的蛋糕。
這種編譯模型是最直觀的,所以編譯器都支持。
而分別編譯模型只有小部分編譯器支持,然而新的C++標准已經把支持這一方式的export關鍵字去掉了,因此此處不再討論。
非類型模板參數實參必須是編譯時常量表達式。
templateclass Test { public: Test(): m_height(height), m_width(width) {} private: int m_height; int m_width; }; int main() { Test<80, 60> myTest; }
MyStack模板接受兩個參數,第一個參數是T,第二個參數還是一個類模板DataContainer,該類模板DataContainer接受兩個參數ContainT和AllocT,默認值類型為vector。
同樣的,模板類裡面的成員函數也可以是模板。
stack1和stack2是兩種不同的類型,它們的賦值操作能夠成功,是因為其operator = 操作符接收的兩個參數分別是MyStack< T >和MyStack< T2 >。
它可以讓我們將模板的聲明和定義分開放置。
//test.h templateclass Test { public: void setData(const T&); const T getData(); private: T m_value; }; //test.cpp #include "test.h" template void Test ::setData(const T& value) { m_value = value; } template const T Test ::getData() { return m_value; } //顯式實例化 template class Test ; //錯誤語法 //template Test ; //template<> Test ;
上面的一個缺陷就是,我們必須為我們要用到的類型實例化,比如針對上述代碼,如果我們使用Test< long >,將會出錯。
在C++編譯器內執行並於編譯完成時停止執行的程序——它將工作從運行期轉移到編譯期,因此某些原本需要在運行期才能檢測到的錯誤,現在能夠在編譯器找出來。當然,不可避免地,編譯時間將會變長。
一個很典型的例子就是計算斐波那契數列:
templatestruct Factorial { enum { value = n * Factorial ::value }; }; template<> //特殊情況 struct Factorial<0> { enum { value = 1 }; }
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 != someNastyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
對象w的類型被聲明為Widget,那麼w必須支持Widget的所有接口,我們可以在源碼中找出這個接口(例如Widget的.h文件中)——因此說是顯式接口。
由於Widget的某些成員函數是virtual,因為w對這些函數的調用,將在運行時根據w的動態類型決定調用哪一個函數,表現出運行期多態。
如果我們將doProcessing寫成模板:
templatevoid doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { T temp(w); temp.normalize(); temp.swap(w); } }
w必須支持哪一種接口呢?這是由template中執行於w身上的操作來決定,本例中它至少需要支持size,normalize和swap成員函數、copy構造函數、不等比較。
凡涉及w的任何函數調用,都有可能造成template實例化,使這些調用得以成功,這些實例化發生在編譯期。
顯式接口與隱式接口的差異:顯式接口基於函數簽名式,而隱式接口由有效表達式組成。
什麼意思呢?
一個例子來看一下:
if (w != someNastyWidget)
T的隱式接口似乎有以下約束:
它必須支持一個operator !=的函數,用來比較兩個T對象(我們假設someNastyWidget的類型為T)。
而事實上,T並不需要支持operator !=,因為該表達式可能成立:operator !=接受一個類型為X的對象和一個類型為Y的對象,T可被轉換為X而someNastyWidget可被轉換為Y,這樣就可以有效調用operator !=了。
一般情況下,在template聲明式中,class和typename是等價的,並沒有什麼不同。
特殊情況是,typename被用來驗明嵌套從屬類型的名稱。
templatevoid print2nd(const C& container) { C::const_iterator* x; ... }
看起來好像我們聲明x為一個變量,它是一個指針,指向一個C::const_iterator,然而編譯器可不這麼理解。
也許C有個static成員變量恰好被命名為const_iterator,如果恰好x是個global變量名稱呢,那麼上述代碼可能被理解為C::const_iterator乘以x——聽起來有些瘋狂,但是完全是有可能的。
C++有個規則可以解析此歧義狀態:如果解析器在template中遭遇一個嵌套叢書名稱,它便假設這名字不是個類型,除非你告訴它。
那麼,要如何告訴編譯器這是一個類型呢?——只要緊臨它之前放置關鍵字typename即可:
templatevoid print2nd(const C& container) { typename C::const_iterator* x; //這才是合法的C++代碼 ... }
typename必須作為嵌套從屬類型名的前綴詞有一個例外:
typename不能出現在base classes list內的嵌套從屬類型名之前,也不能出現在成員初始化列表中作為base class的修飾符。例如:
templateclass Derived: public Base ::Nested //不允許typename { public: explicit Dervied(int x) : Base ::Nested(x) //不允許typename { typename Base ::Nested temp; //必須typename ... } };
SFINAE是C++的一個特性。
我們都知道對於非模板函數的重載來說,無論是否被調用,或是無論調用點需要的是什麼類型的重載,編譯器會將所有參與了重載的函數一個不落的全部編譯。而且這些函數的所有信息已經具備,當進行調用的時候,編譯器就能根據參數的個數跟類型來調用相關度最高的函數。
但對於模板函數來說就不一樣了,因為事先編譯器根本無法獲得所有信息,編譯器也不可能為所有重載的模板函數生成真正的執行代碼,而是會選擇最相關的模板函數進行實例化。
C++中,函數模板與同名的非模板函數重載時,應遵循下列調用原則:
尋找一個參數完全匹配的函數,若找到就調用它。若參數完全匹配的函數多於一個,則這個調用是一個錯誤的調用。
尋找一個函數模板,若找到就將其實例化生成一個匹配的模板函數並調用它。
若上面兩條都失敗,則使用函數重載的方法,通過類型轉換產生參數匹配,若找到就調用它。
若上面三條都失敗,還沒有找都匹配的函數,則這個調用是一個錯誤的調用。
看下面的例子:
#includeusing namespace std; void print( int iNum ) { cout<<"int print( int )"<< endl; } template < typename T > void print( T type ) { typename T::value_type vt_someval; cout<<"template < typename T >"<< endl; } int main() { short sNum = 10; print( sNum ); return 0; }
以上代碼將出現編譯錯誤:
In function ‘void print(T) [with T = short int]’: instantiated from here 錯誤:‘short int’不是類、結構或聯合類型
根據上面的匹配規則,先查找void print( short iNum ),結果沒找到,於是尋找模板,發現有void print( T type ),於是選擇了它進行實例化,但是在實例化過程中卻發現short不是類、結構或聯合類型,所以short::value_type將造成編譯失敗。
以上情況是匹配成功了——但是實例化失敗。
修改以上代碼為以下形式:
#includeusing namespace std; void print( int iNum ) { cout<<"int print( int )"<< endl; } template < typename T > void print( T type, typename T::value_type* pvt_dummy = NULL ) { typename T::value_type vt_someval; cout<<"template < typename T >"<< endl; } int main() { short sNum = 10; print( sNum ); return 0; }
這時候發現能夠編譯成功,輸出的信息為:int print(int)。
這次為什麼能夠成功調用void print(int iNum)呢?
對於第一次的代碼,由於對於模板函數來說,返回值跟參數都能匹配成功,就表示編譯器會認為特化成功而選擇模板函數進行特化進而放棄其他選擇,然而在實例化的時候自然會產生錯誤。但是多了typename T::value_type*後,編譯器在匹配的時候就會發現錯誤。這時由於SFINAE的存在,編譯器就會放棄特化轉而去選擇void print(int INum),而不是簡單報錯。
一個簡單的應用:
目標:在模板推導過程中,得到正確的類型或表達式。
首先,我們構造不同的結構數據,它們分別代表兩種意義:
typedef char TrueType; typedef struct{char a[2];} FalseType;
然後定義下面兩個函數:
templatestatic TrueType TestIsClass(int C::*); //參數是int*類型,在C作用於下,返回類型為TrueType template static FalseType TestIsClass(...); // 參數任意,返回類型為FalseType
然後依賴編譯期間的操作比如sizeof來快速區分當前的數據類型:
templateclass IsClass { public: enum { OK = (sizeof(TestIsClass (0)) == sizeof(TrueType)) }; };
我們用兩個類型MyTest和int來做分析,看看編譯時發生了什麼:
代碼中使用了 IsClass< MyTest >,於是編譯器進行實例化:
class IsClass{ public: enum { OK = (sizeof(TestIsClass (0)) == sizeof(TrueType)) }; //這裡的計算將會在編譯時完成,因此編譯結束後,OK只是一個true或者false而已。 };
而在實例化TestIsClass< MyTest >時,首先尋找完全匹配,沒找到,於是找模板,找到並成功匹配了下面的模板:
templatestatic TrueType TestIsClass(int C::*);
而sizeof計算的是函數的返回值的大小,因此其計算變為:
enum { OK = (sizeof(TrueType) == sizeof(TrueType)) };
最終當然OK為true。
而如果代碼使用了 IsClass< int >,於是編譯器進行實例化:
class IsClass{ public: enum { OK = (sizeof(TestIsClass (0)) == sizeof(TrueType)) }; };
而在實例化TestIsClass< int >時,首先尋找完全匹配,沒找到,於是找模板,由於int不是類或者結構體或者聯合類型,因此模板也匹配失敗,最終匹配到了任意參數的函數版本。(此處有賴於SFINAE)
因此其計算變為:
enum { OK = (sizeof(FalseType) == sizeof(TrueType)) };
最終當然OK為false。
可以看到,所以的行為都在編譯時就完成了,最終的二進制代碼中,恐怕只留下OK = true或者OK = false了。
於是我們順利在編譯時就完成了內置類型還是類類型的一個區分。
本文僅針對部分最近學習到的template知識進行總結,這並不是template的全部。Template C++還有許多高深的內容值得學習,在此之前,我對C++的印象就是——面向對象,接觸了模版之後,才發現C++的另一面——泛型編程也是十分博大而且有趣。