c++元編程模板函數重載匹配規則示例詳解
前言
模板元編程,是一個聽起來非常硬核的概念,會感覺這個東西非常的難,是大佬才能掌握的內容。而事實上,他也確實不簡單(手動狗頭),但是也並沒有想象中的復雜。
我們對很多事物,都喜歡加上“元”的概念,如學習,指的是學習知識,比如學習數學。而元學習,指的是學習學習本身,去學習如何更好地學習,也就是提升學習能力。所以“元”概念,在很多時候值得就是把關註對象回到本身,比如上面的例子,把關註對象從數學等知識回到學習本身。
模板編程,指的是可以我們可以將函數或者類的數據類型抽離出來,做到類型無關性。我們關註的對象,是普通函數、普通類。如下面的這個經典的模板函數:
template<typename T> bool compare(T t1,T t2) { return t1 > t2; }
我們可以使用一份代碼,來判斷兩個相同的類型的對象,t1是否大於t2。
而模板元編程,則是對模板函數、模板類本身,進行編程。繼續上面的代碼例子,假如有一些類型,他並沒有>
運算符,隻有<=
運算符,那麼我們需要重載兩個模板函數,對這兩個類型的數據進行分類:
// 函數1 template<typename T> bool compare(T t1,T t2) { return t1 > t2; } // 函數2 template<typename T> bool compare(T t1,T t2) { return t2 <= t1; }
擁有>
運算符的類型進入函數1,擁有<=
運算符進入函數2。我們這裡對模板類型進行判斷、選擇的過程,就是模板元編程。可以說,模板編程,是將數據類型從函數或者類抽離出來;而模板元編程,則是對類型進行更加細致的劃分,分類別進行處理。
這個時候可能有讀者會有疑問:這不就是類型識別嗎?我用typeId
也可以實現啊,例如以下代碼:
template<typename T> void show(T t) { if(typeid(T).hash_code()==...) { t.toString(); } else { t.toType(); } }
這種寫法是錯誤的。上面代碼例子中無法通過編譯,原因是T
類型無法同時擁有toString()
和toType()
函數,即使我們的代碼隻會運行其中一個路徑。其次:
typeid
在多動態庫環境下,會出現不一致的問題,並不是非常可靠。typeid
隻能對已有的數據類型進行判斷,無法判斷新增類型。- 會導致函數臃腫,判斷條件眾多,代碼不夠優雅。
原因有很多,這裡列舉瞭幾條,一句話總結就是不可靠、不適用、不優雅。因此我們才需要模板元編程。
那麼,如何在模板中實現對類型的判斷並分類處理呢?我們接著往下看。
文章內容略長,我非常建議你完整閱讀,但是如果時間比較緊,可以選擇性閱讀章節:
開始:從一個具體的例子從0到1解析模板元編程
模板函數重載匹配規則+模板匹配規則:介紹模板編程最核心的兩個規則,他是整個模板元編程依賴的基礎
最後的章節進行全文的總結
開始
我們先從一個例子來看模板元編程是如何工作的。我們創建一個類HasToString
,其作用是判斷一個類型是否有toString
成員函數,使用的代碼如下:
template<typename T> HasToString{...} class Dog { }; class Cat { public: std::string toString() const{ return "cat"; } }; std::cout << "Dog:" << HasToString<Dog>::value << std::endl; // 輸出0 std::cout << "Cat:" << HasToString<Cat>::value << std::endl; // 輸出1
通過類HasToString
,我們可以判斷一個類型是否有toString
這個成員函數。好,接下來讓我們看一下HasToString
是如何實現的:
// 判斷一個類型是否有 toString 成員函數 template<typename T> class HasToString { template<typename Y, Y y> class Helper {}; template<typename U = T> constexpr static bool hasToString(...) { return false; } template<typename U = T> constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*) { return true; } public: const static bool value = hasToString<T>(nullptr); };
好傢夥,這也太復雜瞭!!完全沒看懂。你是否有這樣的感覺呢?如果你是第一次接觸,感覺比較復雜很正常,現在我們無需完全理解他,下面我們一步步慢慢說。
首先有兩個c++的其他知識先解釋一下:constexpr
關鍵字和成員函數指針,瞭解的讀者可以直接跳過。
constexpr:表示一個變量或者函數為編譯期常量,在編譯的時候可以確定其值或者函數的返回值。在上面的代碼中,const static bool value
需要在編譯器確定其值,否則不能在類中直接復制。因此我們給hasToString
函數增加瞭constexpr
關鍵字。
成員函數指針:我們可以獲取一個對象的成員函數指針,而在合適的時候,調用此函數。如下代碼
std::string (Cat::*p)() const = &Cat::toString; // 獲取Cat的函數成員指針 Cat c; std::string value = (c.*p)(); // 通過成員函數指針調用c的成員函數
可以看到成員函數指針的聲明語法和函數指針很相似,隻是在前面多瞭Cat::
表示是哪個類的指針。
這裡僅簡單介紹,其他更詳細的內容,感興趣可以百度一下瞭解。
好,我們第一步先看到HasToString
的value
變量,他是一個const static bool
類型,表示T
類型是否有toString
函數的結果。他的值來源於hasToString<T>(nullptr)
,我們繼續看到這個函數。
hasToString
是一個返回值為bool
類型的模板函數,由於其為constexpr static
類型,使得其返回值可以直接賦值給value
。他有兩個重載實例:
- 第一個重載函數的參數為函數參數包
- 第二個重載函數的參數為Helper對象的的指針
我們暫時先不管Helper
的內容,當我們調用hasToString<T>(nullptr)
時,他會選擇哪個重載函數?答案是不管T
類型如何,都會先進入第二個重載函數。原因是,第二個重載函數相比第一個更加特例化:實參與形參均為指針類型,根據模板函數匹配規則,他的優先級更高,因此會選擇第二個重載函數進行匹配。
到這裡,我們已經可以明確,在編譯時,不管T
的類型如何,均會調用到hasToString
的第二個重載函數。這個時候,我們看到模板類Helper
,他的模板類型很簡單,第一個模板參數是Y
,而第二個模板參數則為第一個模板類型的對象值。
看到hasToString
第二個重載函數,其參數為一個Helper
類型指針。其中,Helper
的第一個模板類型描述瞭成員函數toString
的函數類型,第二個模板參數獲取模板類型U
的成員函數toString
的指針。這一步可以保證類型U
擁有成員函數toString
,且類型為我們所描述的函數類型。
好,到這裡就可能有兩種情況:
- 假如類型
U
擁有toString
成員函數,那麼函數匹配正常,hasToString
實例化成功。 - 假如類型
U
沒有toString
成員函數,此時會匹配失敗,因為&U::toString
無法通過編譯。這個時候,根據c++的模板匹配規則,匹配失敗並不會直接導致崩潰,而是會繼續尋找可能的函數重載。
對於類型Dog
,他沒有toString
成員函數,hasToString
第二個重載函數匹配失敗,此時會繼續尋找hasToString
的其他重載類型。到瞭第一個重載類型,匹配成功,類型Dog
匹配到hasToString
第一個重載函數。
這裡就是我們整個HasToString
的重點:他成功將含toString
成員函數的類型,與不含toString
成員函數的類型成功分到兩個不同重載函數中去,完成我們判斷的目的。
這,就是模板元編程。
好瞭,對於一開始我們覺得很復雜的代碼,我們也基本都瞭解瞭,可以先暫時松一口氣,先來回顧一下上面的內容:
// 判斷一個類型是否有 toString 成員函數 template<typename T> class HasToString { template<typename Y, Y y> class Helper {}; template<typename U = T> constexpr static bool hasToString(...) { return false; } template<typename U = T> constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*) { return true; } public: const static bool value = hasToString<T>(nullptr); };
- 我們創建瞭一個模板類
HasToString
來判斷一個類型是否擁有toString
成員函數,並將結果存儲在靜態常量value
中。 value
的值來源於靜態模板函數hasToString
的判斷,我們將該函數設置為constexpr
類型,因此可以直接將返回值賦值給value
。- 利用模板函數重載匹配規則,將函數調用優先匹配到
hasToString
的第二個重載函數進行匹配。 - 我們創建瞭
Helper
輔助模板類,來描述我們需要的成員函數類型,並獲取類型的成員函數。 - 利用模板匹配規則,匹配失敗的類型,將進入
hasToString
的第一個重載函數進行匹配,實現類型的選擇。
整個過程最核心的部分,是模板函數hasToString
的重載與匹配。而其所依賴的,是我們重復提到模板函數重載匹配規則、模板匹配規則,那麼接下來,我們來聊聊這個匹配規則的內容。
模板函數重載匹配規則
模板函數重載匹配規則,他規定著,當我們調用一個具有多個重載的模板函數時,該選擇哪個函數作為我們的調用對象。與普通函數的重載類似,但是模板屬性會增加一些新的規則。
模板函數重載匹配規則可以引用《c++ primer》中的一段話來總結:
對於一個調用,其候選函數包括所有模板實參推斷成功的函數模板實例。
候選的函數模板總是可行的,因為模板實參推斷會排除任何不可行的模板。
與往常一樣,可行函數(模板與非模板)按類型轉換 (如果對此調用需要的話)來排序。當然,可以用於函數模板調用的類型轉換是非常有限的。
與往常一樣,如果恰有一個函數提供比任何其他函數都更好的匹配,則選擇此函數。 但是,如果有多個函數提供同樣好的匹配,則:
- 如果同樣好的函數中隻有一個是非模板函數,則選擇此函數。
- 如果同樣好的函數中沒有非模板函數,而有多個函數模板,且其中一個模板比其他模板更特例化,則選擇此模板。
- 否則,此調用有歧義。
看著有點不知所以然,我們一條條來看。這裡我給整個過程分為三步:
第一步:模板函數重載匹配會將所有可行的重載列為候選函數。
舉個例子,我們現在有以下模板函數以及調用:
template<typename T> void show(T t) {...} // 形參為T template<typename T> void show(T* t) {...} // 形參為T* int i = 9; show(i); show(&i);
代碼中模板函數show
有兩個重載函數,其形參不同。當調用show(i)
時,第一個重載函數T
可以匹配為int
類型,第二重載函數,無法完成int
類型到指針類型的匹配,因此本次調用的候選重載函數隻有第一個重載函數。
第二個調用show(&i)
,第一個重載函數T
可以匹配為int*
類型,第二個重載函數T
可以匹配為int
類型,因此本地調用兩個重載函數都是候選函數。
選擇候選函數是整個匹配過程的第一步,過濾掉那些不符合的重載函數,再進行後續的精確選擇。
第二步:候選可行函數按照類型轉換進行排序
匹配的過程中,可能會發生類型轉換,需要類型轉換的優先級會更低。看下面代碼:
template<typename T> void show(T* t) {...} // 形參為T* template<typename T> void show(const T* t) {...} // 形參為const T* int i = 9; show(&i);
show
兩個重載函數均作為候選函數。第一個函數的形參會被匹配為int*
,而第二個重載函數會被匹配為const int*
,進行瞭一次非const指針到const指針的轉換。因此前者的優先級會更高。
類型轉換,主要涉及volatile
和const
轉換,上面的例子就是const
相關的類型轉換。類型轉換是匹配過程中的第二步。
此外,還有char*
到std::string
的轉換,也屬於類型轉換。字符串字面量,如"hello"
屬於const char*
類型,編譯器可以完成到std::string
的轉化。
第三步:若第二步存在多個匹配函數,非模板函數優先級更高;若沒有非模板函數,則選擇特例化更高的函數。
到瞭這一步,基本選擇出來的都是精確匹配的函數瞭。但是卻存在多個精確匹配的函數,需要按照一定規則進行優先級排序。看下面例子代碼:
template<typename T> void show(T t) {...} // 形參為T template<typename T> void show(T* t) {...} // 形參為T* void show(int i) {...} // 非模板函數 int i = 9; show(i); show(&i);
在上面代碼中,show(i)
的調用,有兩個精確匹配的函數,第一個和第三個重載函數。但是,第三個重載函數為非模板函數,因此其優先級更高,選擇第三個重載函數。
show(&i)
調用中,可以精確匹配到第一個和第二個重載函數。但是第二個函數相比第一個會更加特例化,他描述的形參就是一個指針類型。因此選擇第二個重載函數版本。
到此基本就能選擇最佳匹配的重載函數版本。若最後出現瞭多個最佳匹配,則本地調用時有歧義的,調用失敗。
這裡需要註意的一點是,引用不屬於特例化的范疇,例如以下的代碼在調用時是有歧義的:
template<typename T> void show(T t) {...} // 形參為T template<typename T> void show(T& t) {...} // 形參為T& int i = 9; show(i); // 調用失敗,無法確定重載版本
好瞭,這就是整個模板函數重載的匹配過程,主要分三步:
- 選擇所有可行的候選重載函數版本
- 根據是否需要進行類型轉換進行排序
- 優先選擇非模板類型函數;若無非模板函數則選擇更加特例化的模板函數。若出現多個最佳匹配函數則調用失敗
瞭解瞭模板函數重載的匹配過程,那麼我們就能在進行模板元編程的時候,對整體的匹配過程有把握。除瞭模板函數重載匹配規則,還有一個重要的規則需要介紹:模板匹配規則。
模板匹配規則
模板,有兩種類型,模板函數和模板類。模板類沒有和模板函數一樣的重載過程,且在使用模板類時需要指定其模板類型,因此其貌似也不存在匹配過程?不,其實也存在一種場景具有類似的過程:默認模板參數。看下面的例子:
template<typename T,typename U = int> struct Animal {}; template<typename T> struct Animal<T,int> {}; Animal<int> animal;
模板類Animal
有兩個模板參數,第二個模板參數的默認類型為int。代碼中特例化瞭<T,int>
類型,與第二個模板參數的默認值保持一致。當我們使用Animal<int>
實例化時,Animal
兩個模板參數被轉化為<int,int>
,模板匹配會選擇特例化的版本,也就是template<typename T> struct Animal<T,int>
版本。這個過程有點類似我們前面的模板函數重載匹配過程,但是本質上是不同的,模板類的匹配過程不涉及類型轉換,完全是精確類型匹配。但在行為表現上有點類似,因此在這裡補充說明一下。
這裡我們要介紹一個更加重要的規則:SFINAE法則。
這個法則很簡單:模板替換導致無效代碼,並不會直接拋出錯誤,而是繼續尋找合適的重載。我們還是通過一個例子來理解:
// 判斷一個類型是否有 toString 成員函數 template<typename T> class HasToString { template<typename Y, Y y> class Helper {}; template<typename U = T> constexpr static bool hasToString(...) { return false; } template<typename U = T> constexpr static bool hasToString(Helper<std::string (U::*)() const,&U::toString>*) { return true; } public: const static bool value = hasToString<T>(nullptr); };
這是我們前面的例子,當我們調用hasToString<T>(nullptr)
時,模板函數hasToString
的兩個重載版本都是精確匹配,但是後者為指針類型,更加特例化,因此優先選擇第二個重載版本進行替換。到這裡應該是沒問題的。
但是,如果我們的類型T
不含toString
成員函數,那麼在這個部分Helper<std::string (U::*)() const,&U::toString>
會導致替換失敗。這個時候,按照SFINAE法則,替換失敗,並不會拋出錯誤,而是繼續尋找其他合適的重載。在例子中,雖然第二個重載版本替換失敗瞭,但是第一個重載版本也是精確匹配,隻是因為優先級沒有第二個高,這個時候會選擇第一個重載版本進行替換。
前面我們在講模板函數重載規則時提到瞭候選函數,在匹配完成後發生替換失敗時,會在候選函數中,按照優先級依次進行嘗試,直到匹配到替換成功的函數版本。
這一小節前面提到的模板類的默認模板參數場景,也適用SFINAE法則。看下面的例子:
class Dog {}; template<typename T,typename U = int> struct Animal {}; template<typename T> struct Animal<T, decltype(declval<T>().toString(),int)> {}; Animal<Dog> animal;
代碼中有一個關鍵字declval
,有些讀者可能並不熟悉。
declval的作用是構建某個類型的實例對象,但是又不能真正去執行構建過程,一般結合decltype使用。例如代碼中的例子,我們利用declval構建瞭類型T的實例,並調用瞭其toString的成員函數。使用decltype保證這個過程並不會被執行,僅做類型獲取,或者匹配的過程。更詳細的建議讀者搜索資料進一步瞭解,declval是c++14以後的新特性,如果是c++11則無法使用。
根據前面的內容,我們知道Animal<Dog>
會匹配到特例化的版本,但是由於Dog
類型沒有toString
成員函數,會導致替換失敗。這時候會回到第一個非特例化的版本,進行替換。
好瞭,通過這兩個例子,讀者應該也能理解SFINAE法則的內容。模板重載匹配規則,是整個模板元編程中最核心的內容,利用這個規則,就可以在整個匹配的流程的不同的重載中,函數重載或者類特例化,選擇我們需要的類型,並將其他不需要的類型根據匹配流程繼續尋找匹配的目標,從而完成我們對數據類型的選擇。
這個過程其實有點類似於流轉餐廳:廚師放下的食物是數據類型,每個客戶是重載版本,流水線是模板匹配規則流程,每個客戶選擇自己喜愛的食物,並將不感興趣的食物利用流水線往後傳,每個食物最終都到瞭感興趣的客戶中。當然如果最終無人感興趣,則意味著匹配出錯。
使用
到此,我們對於模板元編程核心內容就瞭解完成瞭。那麼在實際中如何去使用呢?這裡給出筆者的一些經驗。
首先,必須要明確目的,不要為瞭使用技術而使用技術。模板元編程,能完成的功能是,在模板重載中實現對類型的判斷與選擇。當我們有這個需求的時候,可以考慮使用模板元編程,這裡舉幾個常見場景。
我們回到我們最開始的那個例子:比較大小。假如一個類型擁有<
操作,采用<
運算符進行比較,否則采用>=
運算符進行比較。這裡我們采用默認模板參數的方式進行編寫:
template<typename T,typename U = int> struct hasOperate { constexpr static bool value = false; }; template<typename T> struct hasOperate<T, decltype(declval<T>() < declval<T>(),int())> { constexpr static bool value = true; };
這樣通過value值就可以獲取到結果。那麼我們很容易寫出下面的代碼:
template<typename T> bool compare(const T& t1,const T& t2) { if(hasOperate<T>::value) { return t1 < t2; } else { return t2 >= t1; } }
好瞭,大功告成。運行一下,誒,怎麼編譯不過?這個問題在文章前面有簡單提到。對於類型T
,他可能隻有兩種操作符其中的一種,例如以下類型:
class A { public: explicit A(int num) : _num(num){} bool operator<(const A& a) const{ return _num < a._num; } int _num; };
A類型隻有<
操作符,並沒有>=
操作符,上面的模板函數實例化之後會變成下面的代碼:
bool compare(const A& t1,const A& t2) { if(hasOperate<A>::value) { return t1 < t2; } else { return t2 >= t1; // 這裡報錯,找不到>=操作符 } }
代碼中,即使我們的else邏輯不會運行到,但編譯器會檢查所有關於類型A的調用,再拋出找不到操作符的錯誤。那麼我們該如何操作呢,有兩個思路。
第一個思路是直接在hasOperate
結構體中,分別編寫各自的處理函數。這樣能解決一些問題,但是局限性比較大,不夠靈活。
另一個思路就是我要給你介紹的一個非常好用工具類std::enable_if
。有瞭它之後我們可以這麼使用:
template<typename T> bool compare(typename std::enable_if<hasOperate<T>::value,T>::type t1,T t2) { return t1 < t2; } template<typename T> bool compare(typename std::enable_if<!hasOperate<T>::value,T>::type t1,T t2) { return t2 >= t1; }
感覺有點不太理解,沒事,我們先來瞭解一下他。enable_if
的實現代碼很簡單:
template<bool enable,typename T> struct enable_if {}; template<typename T> struct enable_if<true,T> { using type = T; };
他是一個模板結構體,第一個參數是一個佈爾值,第二個是一個泛型T
。其特例化瞭佈爾值為true
的場景,並增加瞭一個type
別名,反之如果佈爾值為false,則沒有這個type
類型。
回到我們前面使用代碼,我們使用hasOperate<T>::value
來獲取該類型是否擁有指定操作符,如果沒有則獲取不到type類型,那麼整個替換過程就會失敗,需要繼續尋找其他的重載。這樣就實現對類型的選擇。
系統庫中,還提供瞭很多類型判斷接口可以和enable_if
一起使用。例如判斷一個類型是否為指針std::is_pointer<>
、數組std::is_array<>
等。例如我們可以創建一個通用的析構函數,根據是否為數組類型進行析構:
template<typename T> void deleteAuto(typename std::enable_if<std::is_array<T>::value,T>::type t) { delete[] t; } template<typename T> void deleteAuto(typename std::enable_if<!std::is_array<T>::value,T>::type t) { delete t; } int array[9]; int *pointer = new int(1); deleteAuto<decltype(array)>(array); // 使用數組版本進行析構 deleteAuto<decltype(pointer)>(pointer);// 使用指針版本進行析構
結合模板具體化與enable_if
,也可以實現對一類數據的篩選。例如我們需要對數字類型進行單獨處理。首先需要編寫判斷類型是否為數組類型的代碼:
template<typename T> constexpr bool is_num() { return false; } template<> constexpr bool is_num<int>() { return true; } template<> constexpr bool is_num<float>() { return true; } template<> constexpr bool is_num<double>() { return true; } ...
註意這裡的函數必須要聲明為constexpr
,這樣才能在enable_if
中使用。補充好所有我們認為是數字的類型,就完成瞭。使用模板類也是可以完成這個任務的:
template<typename T> struct is_num { constexpr static bool value = false; }; template<> struct is_num<int> { constexpr static bool value = true; }; ... // 補充其他的數字類型
使用靜態常量來表示這個類型是否為數字類型。靜態常量也可以使用標準庫的類,減少代碼量,如下:
template<typename T> struct is_num : public false_type {}; template<> struct is_num<int> : public true_type{}; ... // 補充其他的數字類型
改為繼承的寫法,但原理上是一樣的。
有瞭以上的判斷,就可以使用enable_if
來分類處理我們的邏輯瞭:
template<typename T> void func(typename std::enable_if<is_num<T>(),T>::type t) { //... } template<typename T> void func(typename std::enable_if<!is_num<T>(),T>::type t) { //... }
使用enable_if
的過程中,還需要特別註意,避免出現重載歧義,或者優先級問題導致編程失敗。
最後,再補充一點關於匹配過程的類型問題。還是上面判斷是否是數字的例子,看下面的代碼:
int i = 9; int &r = i; func<decltype<r>>(r); // 無法判斷是數字類型
在我們調用func<decltype<i>>(i);
時,i
的類型是const int
,而我們具體化是template<> constexpr bool is_num<int>() { return true; }
,他的模板類型是int
,這是兩個不同的類型,無法對應。因此判斷此類型為非數字類型。
導致這個問題不止有const
,還有volatile
和引用類型。如int&
、volatile int
等。解決這個問題的方法有兩個:
- 在具體化中,增加
const int
等類型,但是枚舉所有的類型非常繁雜且容易遺忘。 - 在匹配之前,對數據類型進行去修飾處理。
第二種方法,c++提供函數處理。std::remove_reference<T>::type
移除類型的引用,std::remove_cv<T>::type
移除類型的const volatile
修飾。因此我們在調用前可以如此處理:
template<typename T> using remove_cvRef = typename std::remove_cv<typename std::remove_reference<T>::type>::type; int i = 9; int &r = i; func<remove_cvRef<decltype<r>>(r); // 移除引用修飾,轉化為int類型
關於類型推斷相關的問題這裡不多展開,但要特別註意由於類型修飾導致的匹配失敗問題。
最後
文章真的長呀,如果你能堅持看到這裡,說明你是一個非常堅持且對編程有強烈興趣的人,希望這篇文章讓你在c++模板的路上有所幫助。
那麼接下來我們再來回顧一下這篇文章的內容。
- 我們先介紹瞭模板元編程要解決的場景與問題
- 然後我們從一個具體的模板元編程例子展開,一步步學習瞭模板元編程的整體內容
- 接下來針對其核心:模板函數重載匹配規則以及模板規則進一步瞭解
- 最後再給出在使用方面的一些經驗供參考
模板元編程他要解決的最核心的問題就是:對模板類型的判斷與選擇。而其所依賴的最核心的內容是模板函數重載匹配規則以及SFINAE法則,他是我們模板元編程得以實現的基礎。需要註意,整個元編程發生在編譯期,任何的函數調用都無法通過編譯。其次需要類型的推斷導致的匹配錯誤問題,而且此錯誤比較隱蔽難以發現。
最後,模板元編程十分強大,但涉及的相關內容多,容易出錯。隻有當我們十分確定要使用模板元編程解決的問題,再去使用他。切不可為瞭使用而使用,成為自己炫技的工具,這會給代碼留下很多的隱患。
參考
- An introduction to C++'s SFINAE concept: compile-time introspection of a class member:這是國外微軟c++工程師Jean Guegant寫的一篇文章,內容非常好,比較完整地介紹瞭模板元編程,從最基礎的寫法到使用c++11、c++14特性等,非常專業。但是文章僅有英文版本,不建議直接網頁翻譯,有點地方翻譯錯誤無法理解。
- 《c++ primer》:c++學習神書,應該沒有疑問?個人建議如果不是完全沒有編程基礎,使用《c++ primer》來替代《c++ primer plus》吧。
以上就是c++元編程模板函數重載匹配規則示例詳解的詳細內容,更多關於c++元編程模板函數的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 淺談C++中const與constexpr的區別
- 詳解C++中的const和constexpr
- C++ 輕量級對象JSON序列化實現詳情
- C++ decltype用法舉例說明
- C++ decltype 說明符