SFINEA in C++
作者:唐風(fēng)
原載于:www.cnblogs.com/liyiwen
SFINAE(substitution failure is not a error) 主要用于模板函數(shù),它是指,編譯器在使用具體類(lèi)型來(lái)替換模板類(lèi)型參數(shù),對(duì)模板進(jìn)行實(shí)例化(展開(kāi)模板)時(shí),如果發(fā)生替換失敗,那么并不會(huì)直接引發(fā)編譯錯(cuò)誤(Error),而只是簡(jiǎn)單地把這個(gè)模板從重載候選者中去除掉。
還是看看代碼吧(一個(gè)在SFINAE中常遇到的例子):
代碼段1:
template <typename T>
bool is_class(int T::*) {
return true;
}
template <typename T>
bool is_class(...) {
return false;
}
struct Test {
};
int main(void) {
std::cout<<is_class<Test>(0)<<endl;
std::cout<<is_class<int>(0)<<endl;
}
運(yùn)行的結(jié)果是輸出:
1
0
這表明,如果傳給 is_class 的模板參數(shù)是一個(gè)類(lèi),那么返回 true 的那個(gè)版本就會(huì)被選中,否則false的那個(gè)版本會(huì)被選中。就是因?yàn)镾FINAE在起作用。
為什么要提SFINAE?
僅僅從程序員的角度來(lái)看,程序段1中,對(duì)相應(yīng)函數(shù)選擇的結(jié)果是非常符合直觀的預(yù)期,與普通函數(shù)重載是很相似的感覺(jué)。
例如,對(duì)于下面這兩個(gè)函數(shù):
int max(int a, int b) {return a>b?a:b}
float max(float a, float b) {return a>b?a:b}
int main(void) {
float x1=3.4f, x2=3.6f;
cout<<max(x1, x2);
}
對(duì)于 float 型的參數(shù),float 版本的重載自然會(huì)很被選中。在外觀上看,程序段1是一樣的。那么為什么程序段1就需要特別的 SFNIAE 呢?
我想,對(duì)于普通函數(shù)的重載而言,由于這些函數(shù)的所有信息都已經(jīng)完備,在發(fā)生調(diào)用之前,編譯器已經(jīng)可以完成對(duì)這些函數(shù)的編譯,這些函數(shù)也不可能再被增加任何新的信息,可以直接產(chǎn)生執(zhí)行代碼。在函數(shù)的調(diào)用點(diǎn)上,編譯器只需要根據(jù)參數(shù)信息選擇一個(gè)合適函數(shù)的地址就可以了。
但是,對(duì)于模板函數(shù)重載,情況就不一樣了。我們分析下程序段1中,is_class<int>(0) 這個(gè)調(diào)用,在第一步的選擇中,無(wú)論從模板參數(shù)的個(gè)數(shù)、函數(shù)參數(shù)的個(gè)數(shù)來(lái)看,兩個(gè) is_class 的實(shí)現(xiàn)都可能匹配,由于 int T::* (類(lèi)成員指針)的匹配優(yōu)先級(jí)比 … 的要高,所以編譯器會(huì)先試圖使用第一個(gè)版本進(jìn)行展開(kāi)。但編譯展開(kāi)的結(jié)果時(shí)發(fā)現(xiàn) int::* 是不合法的,于是編譯器就放棄展開(kāi)這個(gè)函數(shù),而取另一個(gè)函數(shù)進(jìn)行展開(kāi),并得到正確的調(diào)用。
所以,在真正發(fā)生調(diào)用(應(yīng)該說(shuō)真正需要被展開(kāi))之前,模板函數(shù)中的信息是不完備的,編譯器無(wú)法為這些模板函數(shù)生成真正的執(zhí)行代碼,而只是進(jìn)行一些很基本、簡(jiǎn)單的檢查。所有的模板都不是“真正的代碼”,它們是編譯器用來(lái)生成代碼的工具。在需要展開(kāi)的時(shí)候,編譯器從合適的候選者中選出優(yōu)先級(jí)最高的一個(gè)來(lái)進(jìn)行實(shí)例化(展開(kāi))。在展開(kāi)后的代碼如果不能正確被編譯(像上面例子中 int::* 這種情況),編譯器只是簡(jiǎn)單地放棄這次展開(kāi),轉(zhuǎn)而尋找其它的模板。試想,如果編譯器在展開(kāi)失敗后,直接產(chǎn)生一個(gè)編譯錯(cuò)誤的話,其它的函數(shù)就沒(méi)有機(jī)會(huì)了,這是非常不合理的,因?yàn)椋?.本次展開(kāi)失敗并不意味著被展開(kāi)的模板代碼就有問(wèn)題,因?yàn)橛闷渌?lèi)型的話還是有可能展開(kāi)成功的。2.本次展開(kāi)失敗并不代表用于展開(kāi)的類(lèi)型無(wú)法找到合適的模板,其它模板可能合用。
所以,我覺(jué)得,SFINEA 的意義就是:
編譯器在每個(gè)調(diào)用點(diǎn)上,只為當(dāng)前需要實(shí)例化的類(lèi)型尋找一個(gè)合適的模板進(jìn)行展開(kāi),而不會(huì)為某一次實(shí)例化而展開(kāi)所有可能合適的重載模板(函數(shù))。
這是編譯器“智能”選擇模板的表現(xiàn)。普通函數(shù)重載則不一樣,無(wú)論是否被調(diào)用,或是無(wú)論調(diào)用點(diǎn)需要的是什么類(lèi)型的重載,編譯器會(huì)將所有參與了重載的函數(shù)一個(gè)不落的全部編譯。如果對(duì)模板也采用同樣的方式,那么模板將受到巨大的局限而失去意義。
有了 SFINEA ,當(dāng)我們?cè)趯?xiě)模板代碼的時(shí)候,就不需要擔(dān)心這些模板在使用某些類(lèi)型進(jìn)行展開(kāi)的時(shí)候會(huì)失敗,從而造成程序編譯錯(cuò)誤,因?yàn)槲覀冎谰幾g器只會(huì)在能展開(kāi)的情況展開(kāi)它們,展開(kāi)失敗的情況下,這些代碼并不會(huì)真正進(jìn)入你的程序中。
好了,在結(jié)束本文之前,我們?cè)倏纯?SFINEA “知名”的一個(gè)例子:
程序段2:
template <typename T>
class is_class {
typedef char one;
typedef struct {char a[2];} two;
template <typename C>
static one test(int C::*);
template <typename C>
static two test(...);
public:
enum {value = sizeof(test<T>(0)) == sizeof(one)};
};
這是模板圣經(jīng)《C++ templates》中的一個(gè)例子(原程序可能不完全一樣),與程序段 1 不同的是,is_class<T>::value 是一個(gè)編譯期的 bool 值,而程序段 1 ,ture 或是 false 是在運(yùn)行期才得到的結(jié)果。is_class<T>::value 這樣的“裝置”(device)經(jīng)常出現(xiàn)在模板編譯中,用于根據(jù)類(lèi)型的某種特性(比如,是不是一個(gè)類(lèi)?)來(lái)選擇不同的模板。boost 中的提供了很多類(lèi)似的 device,再配合 boost::enable_if 來(lái)完成威力巨大的模板編程。
可以說(shuō),SFINEA 幾乎是隨處可見(jiàn)的,不可或缺的重要“原則”。:)
本文完。