原文地址:http://www.gotw.ca/publications/mill17.htm
原作者:Herb Sutter
??為什么函數(shù)模版的全特化是不參與函數(shù)重載的呢?而為什么函數(shù)模版沒有偏特化概念呢蕊玷?其實(shí)是C++語法規(guī)定的司顿,但是在平時的工作過程中,出現(xiàn)過因?yàn)楹瘮?shù)版本不能偏特化困擾我們的工作嗎焙矛?答案是沒有,也許很多人忽略了這個問題,主要是因?yàn)榭梢酝ㄟ^函數(shù)重載來規(guī)避這個問題(或者可以認(rèn)為這不是一個問題)逮壁。另,在《C++ Templates》一書的附錄B部分對重載解析做過簡單介紹粮宛,可以參考閱讀窥淆。
函數(shù)模版重載解析的優(yōu)先級(假設(shè)有普通函數(shù),模版函數(shù)巍杈,模版函數(shù)特化等等復(fù)雜情況):
1,普通函數(shù)忧饭,如果類型匹配,優(yōu)先選中秉氧,重載解析結(jié)束.
(2) 如果沒有普通函數(shù)匹配眷昆,那么所有的基礎(chǔ)函數(shù)模版進(jìn)入候選,編譯器開始平等挑選汁咏,類型最匹配的亚斋,則被選中,注意攘滩,此時才會進(jìn)入第(3)步繼續(xù)篩選帅刊;
(3) 如果第(2)步里面選中了一個模版基礎(chǔ)函數(shù),則查找這個模版基礎(chǔ)函數(shù)是否有全特化版本漂问,如果有且類型匹配赖瞒,則選中全特化版本女揭,重載解析結(jié)束,否則使用(2)里面選中的模版函數(shù)栏饮,重載解析依然結(jié)束吧兔。
(4) 如果第(2)步里面沒有選中任何函數(shù)基礎(chǔ)模版,那么匹配失敗袍嬉,編譯器會報錯境蔼,程序員需要檢查下代碼。
??函數(shù)模版的全特化版本不參與函數(shù)重載解析伺通,并且優(yōu)先級低于函數(shù)基礎(chǔ)模版參與匹配的原因是:C++標(biāo)準(zhǔn)委員會認(rèn)為如果因?yàn)槌绦騿T隨意寫了一個函數(shù)模版的全特化版本箍土,而使得原先的重載函數(shù)模板匹配結(jié)果發(fā)生改變(也就是改變了約定的重載解析規(guī)則)是不能接受的。
函數(shù)模版的全特化到底是哪個函數(shù)基礎(chǔ)模版的特化罐监,需要參考可見原則吴藻,也就是說當(dāng)特化版本聲明時,它只可能特化的是當(dāng)前編譯單元已經(jīng)定義的函數(shù)基礎(chǔ)模版弓柱。
鑒于上面兩個原因沟堡,為何還要進(jìn)行函數(shù)模版全特化把自己搞暈?zāi)兀窟耗悖∫驗(yàn)楹瘮?shù)的全特化的版本和定義一個普通函數(shù)基本上一樣弦叶,把模版聲明去掉即可,而且普通函數(shù)的重載優(yōu)先級最高妇多,也就不會踩一些坑了伤哺。
重要區(qū)別:重載和特化
重要的是要確保我們有以下的條款,因此這里我們快速回顧一下者祖。
在C++里面立莉,有類模板和函數(shù)模板。這兩種模板并不是完全以相同的方式工作的七问,并且在重載上面有明顯的區(qū)別蜓耻。老版本的C++里面類并不支持重載(譯者注:現(xiàn)在也不支持),因此類模版也不支持重載械巡。而一直以來刹淌,C++函數(shù)都是支持重載的,因此函數(shù)模版也就支持重載了讥耗∮泄矗看起來只是一件很自然的事情,例子1進(jìn)行了一個簡單的總結(jié):
// 例1:類模版古程、函數(shù)模版以及重載
//
// 類模版
template<class T> class X {/.../ }; // (a)
// 函數(shù)模版以及函數(shù)模版重載
template<class T> void f( T); // (b)
template<class T> void f( int, T,double ); // (c)
未特化的模版通常也叫做底層基礎(chǔ)模版蔼卡。
此外,基礎(chǔ)模版一般都是可以特化的挣磨。但是特化對于類版本和函數(shù)模版來說有很大的不同雇逞,并且這個差異跟我們下面的討論有比較重要的關(guān)系荤懂。類模版可以偏特化和全特化,而函數(shù)模版只能進(jìn)行全特化塘砸,但是由于函數(shù)模版可以重載节仿,我們通過重載可以獲得和偏特化幾乎相同的效果。下面的代碼說明了這些不同點(diǎn):
// 續(xù)例1:模版特化
//
// (a)模版針對指針類型的偏特化
template<class T> classX<T> { /...*/ };
// (a)模版針對int類型的全特化
template<> class X<int> {/.../ };
// 重載(b)和(c)的獨(dú)立基礎(chǔ)模版
// 需要注意的是掉蔬,它不是(b)的偏特化粟耻,
// 因?yàn)樵诤瘮?shù)模版里面壓根就沒有偏特化的概念
template<class T> void f( T*); //(d)
// (b)模版針對int類型的全特化
template<> void f<int>( int); // (e)
// 普通函數(shù)(非模版函數(shù)),并且是(b)眉踱,(c)和(d)的重載函數(shù),
// 注意不是(e)的重載函數(shù)霜威,原因我們等下就討論
void f( double); // (f)
現(xiàn)在讓我們把討論的焦點(diǎn)放到函數(shù)模版上面谈喳,并且思考一下重載的規(guī)則,看看在不同的情況下到底哪個重載函數(shù)被調(diào)用戈泼。規(guī)則其實(shí)非常簡單婿禽,至少從優(yōu)先級上來說,可以分為典型的兩大類:
非模版函數(shù)(譯注:就是普通函數(shù))的優(yōu)先級最高扭倾,如果一個非模版函數(shù)可以完全匹配所有的參數(shù)類型,則會被優(yōu)先選擇調(diào)用挽绩,即使有適合的函數(shù)模版也可以完全匹配所有的參數(shù)類型(譯注:這里的意思就是說,非函數(shù)模版優(yōu)先級最高唉堪,優(yōu)先選中,雖然函數(shù)模版可能也有適合的唠亚,但是因?yàn)閮?yōu)先級低,因此不會沖突)灶搜。
如果沒有非函數(shù)模版匹配,那么函數(shù)基礎(chǔ)模版作為第二優(yōu)先級進(jìn)入候選割卖,但是具體哪個函數(shù)基礎(chǔ)模版被選擇前酿,還需要根據(jù)兩個點(diǎn)來確定薪者,一點(diǎn)比較肯定就是參數(shù)匹配度要是最高的,另一個點(diǎn)就是根據(jù)一套比較晦澀的規(guī)則來判定哪個模版是“最特殊的”(注:這里用“最特殊的”為了和模版特化區(qū)分開來是有點(diǎn)奇怪的言津,也許是一個不經(jīng)意的誤會吧)攻人。下面我們看下這套晦澀的規(guī)則:
如果很明顯有一個“最特殊的”函數(shù)基礎(chǔ)模版悬槽,那么它就被選中了。如果這個基礎(chǔ)模版恰好還有一個專門為所用的參數(shù)類型特化的版本初婆,那么特化版本優(yōu)先被選中,否則基礎(chǔ)模版被選中磅叛。
而如果有一個同級別的“最特殊的”函數(shù)基礎(chǔ)模版,那么調(diào)用將會發(fā)生二義性(譯注:編譯器不知道選擇哪個)弊琴,編譯器無法決定哪個是最合適的兆龙,此時程序員需要明確指定選擇哪一個基礎(chǔ)函數(shù)模版以消除二義性。
另外敲董,如果沒有合適的函數(shù)基礎(chǔ)模版可以匹配參數(shù)類型紫皇,那么調(diào)用無效(譯注:無法編譯),程序員需要修正代碼腋寨。
說了這么多聪铺,為了加深理解,我們還是一起來看一些例子吧:
// 續(xù)例1:重載解決方案
//
bool b;
int i;
double d;
f( b); // 調(diào)用(b)模版 模版參數(shù)T = bool
f( i, 42, d ); // 調(diào)用(c)模版 T = int
f( &i); // 調(diào)用(d)模版 T = int
f( i); // 調(diào)用(e)模版
f( d); // 調(diào)用(f)函數(shù)
目前為止萄窜,我都是選擇的一些最簡單的情況铃剔,下面我們即將涉足該問題的一些誤區(qū)或者說陷阱,去看看一些復(fù)雜的情況脂倦。
為什么不要特化:Dimov/Abrahams的例子
思考下面的代碼:
// 例子2:顯示特化
//
template<class T> // (a) 一個基礎(chǔ)模版
void f( T );
template<class T> // (b) 另一個基礎(chǔ)模版番宁,(a)的重載版本
void f( T* ); // (函數(shù)模版沒有偏特化;可以用重載替代)
template<> // (c) (b)模版的全特化
void f<>(int*);
// ...
int *p;
f( p); // 調(diào)用(c)
例子2里面最后一行選擇調(diào)用的模版可能正是你所期望的赖阻,不過蝶押,問題是,它為什么是你所期望的結(jié)果呢火欧?如果你期望的原因是錯誤棋电,接下來發(fā)生事情可能令你更加驚訝。也許苇侵,有人可能會說赶盔,我給int指針類型寫了一個專門的特化版本啊,因此這次調(diào)用理所當(dāng)然就應(yīng)該是選這個特化版本啊榆浓,然而于未,這正是錯誤的原因。
再考慮下下面的代碼,這個例子是由Peter Dimov和Dave Abrahams提出的:
// 例子3:Dimov 和Abrahams提供的例子
//
template<class T> // (a) 和之前一樣的基礎(chǔ)模版
void f( T );
template<> // (c) 針對(a)的完全特化
void f<>(int*);
template<class T> // (b) 另一個基礎(chǔ)模版烘浦,對(a)的重載
void f( T* );
// ...
int *p;
f( p); // 調(diào)用的是(b)抖坪!重載解析規(guī)則導(dǎo)致特化版本被忽略了
// 因?yàn)橛谢A(chǔ)模版優(yōu)先匹配了
??如果你也感到吃驚,恭喜你闷叉,你并不是第一個對此感到驚訝的人擦俐。很多專家級別的程序員也對此感到驚訝,其實(shí)理解這個問題關(guān)鍵并不復(fù)雜握侧,那就是函數(shù)全特化并不參與重載蚯瞧!
只有基礎(chǔ)模版是可以重載的(當(dāng)然,也包括非模版函數(shù))品擎。再回顧下前面給出的重載解析規(guī)則埋合,這次我特意高亮了一些關(guān)鍵詞:
……
??如果沒有非函數(shù)模版匹配,那么函數(shù)基礎(chǔ)模版作為第二優(yōu)先級進(jìn)入候選萄传,但是具體哪個函數(shù)基礎(chǔ)模版被選擇饥悴,還需要根據(jù)兩個點(diǎn)來確定盲再,一點(diǎn)比較肯定就是參數(shù)匹配度要是最高的瓣铣,另一個點(diǎn)就是根據(jù)一套比較晦澀的規(guī)則來判定哪個模版是“最特殊的”:
??如果很明顯有一個“最特殊的”函數(shù)基礎(chǔ)模版,那么它就被選中了梦碗。而如果這個基礎(chǔ)模版恰好有一個專門為參數(shù)類型特化的版本洪规,那么特化版本優(yōu)先被選中循捺,否則基礎(chǔ)模版被選中。
……等等念赶。重載解析規(guī)則只會先從基本模版(或者非模版函數(shù))里面進(jìn)行挑選叉谜。當(dāng)某個基礎(chǔ)模版被選中之后踩萎,選擇會被鎖定,此時董栽,編譯器會針對這個基本模版進(jìn)行搜索裆泳,檢查是否恰好有一個更合適的全特化模版,如果有的話运提,那么就會使用這個特化模版闻葵。
重要條款
如果你跟我一樣槽畔,第一次看到這種情況時,可能會問這種問題:“嗯鳞尔,我已經(jīng)針對參數(shù)是int指針類型寫了一個專門的特化版本寥假,而且這里剛好是一個參數(shù)是int類型的調(diào)用霞扬,恰好匹配啊,那為什么我寫的特化版本沒有被選中呢萤彩?”可惜雀扶,呃肆汹,這個想法是錯誤的:如果這種情況下(譯注:指用int*參數(shù)進(jìn)行調(diào)用)你想你的特化版本能被選中,其實(shí)你把特化模版它改成一個普通函數(shù)就可以做到转绷。
??要理解為什么模版函數(shù)的特化(譯注:這里指模版函數(shù)的全特化议经,模版函數(shù)沒有偏特化)不參與函數(shù)重載看起來挺怪異,但是一旦解釋清楚了也很簡單咧织。因?yàn)檫@個原因可能跟你想的恰恰相反:如果因?yàn)槟汜槍σ粋€特殊的模版寫了一個特化版本习绢,就要改變模版的選擇規(guī)則,這回驚訝的就是標(biāo)準(zhǔn)委員會了(譯注:意思就是標(biāo)準(zhǔn)委員會規(guī)定了特化版本不能改變模版選擇規(guī)則)闪萄。在這樣的邏輯或者標(biāo)準(zhǔn)下败去,如果我們已經(jīng)有辦法讓函數(shù)調(diào)用選擇我們想選擇的版本(譯注:譬如上面提到的用普通函數(shù))(我們做的僅僅是讓他成為一個普通函數(shù)烈拒,而不是一個特化),這樣也許我們能夠更加深刻的理解為什么特化版本不會影響模版的選擇過程荆几。
??條款1:如果你正在定制一個函數(shù)基礎(chǔ)模版吨铸,并且希望有重載版本(在一些特殊情況下有專門的精確匹配版本),建議你使用普通的函數(shù)重載,而不要使用特化狈涮。并且歌馍,如果你提供了重載的函數(shù)模版,也盡量不要特化它暴浦。 但是晓锻,你不僅僅是在使用函數(shù)模版砚哆,而是在寫一個函數(shù)模版(譯者注:可以認(rèn)為你是公共庫的作者),那么怎樣才能讓函數(shù)模版的使用者(包括自己),不遇到前面出現(xiàn)的那個問題呢卵史?實(shí)際上搜立,你還可以這樣做(參考條款#2):
??條款2:如果你正在寫一個函數(shù)基礎(chǔ)模版,并且希望它成為一個唯一的函數(shù)模版忧设,絕不對它進(jìn)行特化和重載见转,那么你可以把這個函數(shù)模版實(shí)現(xiàn)為包含一個相同簽名的靜態(tài)函數(shù)的簡單類模版蒜哀。每個人都可以對它進(jìn)行特化和偏特化,但是不會對重載解析規(guī)則產(chǎn)生任何影響乘客。
總結(jié)
??重載函數(shù)模版是一種不錯的方式易核,重載解析過程會平等的對待所有的基礎(chǔ)模版浪默,而且整個解析過程和普通函數(shù)重載是類似的,令人感覺很自然碰逸,符合期望饵史,所有可見的重載函數(shù)模版都納入重載解析規(guī)則胜榔,然后編譯器從里面選出一個最合適的。
??特化函數(shù)模版就是一個不太直觀的事情吭露,首先奴饮,你還不能偏特化函數(shù)模版——原因是C++語言標(biāo)準(zhǔn)規(guī)定了纬向!其次逾条,函數(shù)模版特化后的版本還不參與重載师脂。也就是說,任何函數(shù)模版的特化版本都不影響重載解析過程吃警,這種行為可能違背了大多數(shù)人的直覺啄育。然而,如果你寫了一個非模版函數(shù)用來完全替代某函數(shù)模版的特化版本(譯注:函數(shù)簽名一樣挑豌,其實(shí)把函數(shù)模版的特化版本的模版聲明去掉就變成了普通函數(shù)),這個普通函數(shù)反而會被優(yōu)先選擇氓英,因?yàn)楦鶕?jù)C++標(biāo)準(zhǔn),普通函數(shù)總是應(yīng)該認(rèn)為比模版函數(shù)的匹配度要好铝阐。
??如果你正在寫一個函數(shù)模版,希望它成為一個不會被特化或者重載的唯一的函數(shù)模版练对,并且你在一個類模版中實(shí)現(xiàn)了這個函數(shù)模版吹害,這可能是一個眾所周知的繞過函數(shù)模版局限性和陷阱(譯注:指函數(shù)模版沒有偏特化和前面遇到重載解析的問題)的好辦法。使用這種方法,程序員可以在類模版的基礎(chǔ)上不受限制的使用偏特化和全特化(顯示特化)钟些,并且不會影響到原始的函數(shù)模版的任何期望的動作绊谭。這樣就避免了函數(shù)模版的兩個限制,不能偏特化篙耗,以及某些情況下因?yàn)楹瘮?shù)特化版本不參與重載帶來的令人驚訝的的效應(yīng)。至此脯燃,問題全部解決蒙保。如果你正在使用某人寫的老式的函數(shù)模版(沒有使用上面例子4里面使用類模版包裝的方法),并且想寫一個特殊情況下的重載版本邓厕,不要特化它,只要寫一個相同函數(shù)簽名的普通函數(shù)就可以了补君。