首頁 > 軟體

c++超程式設計模板函數過載匹配規則範例詳解

2023-01-14 14:00:20

前言

模板超程式設計,是一個聽起來非常硬核的概念,會感覺這個東西非常的難,是大佬才能掌握的內容。而事實上,他也確實不簡單(手動狗頭),但是也並沒有想象中的複雜。

我們對很多事物,都喜歡加上“元”的概念,如學習,指的是學習知識,比如學習數學。而元學習,指的是學習學習本身,去學習如何更好地學習,也就是提升學習能力。所以“元”概念,在很多時候值得就是把關注物件回到本身,比如上面的例子,把關注物件從數學等知識回到學習本身。

模板程式設計,指的是可以我們可以將函數或者類的資料型別抽離出來,做到型別無關性。我們關注的物件,是普通函數、普通類。如下面的這個經典的模板函數:

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::表示是哪個類的指標。

這裡僅簡單介紹,其他更詳細的內容,感興趣可以百度一下了解。

好,我們第一步先看到HasToStringvalue變數,他是一個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指標的轉換。因此前者的優先順序會更高。

型別轉換,主要涉及volatileconst轉換,上面的例子就是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++超程式設計模板函數的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com