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