原文地址:http://www.gotw.ca/publications/mill17.htm
原作者:Herb Sutter
??為什么函數(shù)模版的全特化是不參與函數(shù)重載的呢?而為什么函數(shù)模版沒有偏特化概念呢?其實是C++語法規(guī)定的,但是在平時的工作過程中,出現(xiàn)過因為函數(shù)版本不能偏特化困擾我們的工作嗎?答案是沒有,也許很多人忽略了這個問題,主要是因為可以通過函數(shù)重載來規(guī)避這個問題(或者可以認為這不是一個問題)。另,在《C++ Templates》一書的附錄B部分對重載解析做過簡單介紹,可以參考閱讀。
函數(shù)模版重載解析的優(yōu)先級(假設有普通函數(shù),模版函數(shù),模版函數(shù)特化等等復雜情況):
1,普通函數(shù),如果類型匹配,優(yōu)先選中,重載解析結束.
(2) 如果沒有普通函數(shù)匹配,那么所有的基礎函數(shù)模版進入候選,編譯器開始平等挑選,類型最匹配的,則被選中,注意,此時才會進入第(3)步繼續(xù)篩選;
(3) 如果第(2)步里面選中了一個模版基礎函數(shù),則查找這個模版基礎函數(shù)是否有全特化版本,如果有且類型匹配,則選中全特化版本,重載解析結束,否則使用(2)里面選中的模版函數(shù),重載解析依然結束。
(4) 如果第(2)步里面沒有選中任何函數(shù)基礎模版,那么匹配失敗,編譯器會報錯,程序員需要檢查下代碼。
??函數(shù)模版的全特化版本不參與函數(shù)重載解析,并且優(yōu)先級低于函數(shù)基礎模版參與匹配的原因是:C++標準委員會認為如果因為程序員隨意寫了一個函數(shù)模版的全特化版本,而使得原先的重載函數(shù)模板匹配結果發(fā)生改變(也就是改變了約定的重載解析規(guī)則)是不能接受的。
函數(shù)模版的全特化到底是哪個函數(shù)基礎模版的特化,需要參考可見原則,也就是說當特化版本聲明時,它只可能特化的是當前編譯單元已經(jīng)定義的函數(shù)基礎模版。
鑒于上面兩個原因,為何還要進行函數(shù)模版全特化把自己搞暈呢?!因為函數(shù)的全特化的版本和定義一個普通函數(shù)基本上一樣,把模版聲明去掉即可,而且普通函數(shù)的重載優(yōu)先級最高,也就不會踩一些坑了。
重要區(qū)別:重載和特化
重要的是要確保我們有以下的條款,因此這里我們快速回顧一下。
在C++里面,有類模板和函數(shù)模板。這兩種模板并不是完全以相同的方式工作的,并且在重載上面有明顯的區(qū)別。老版本的C++里面類并不支持重載(譯者注:現(xiàn)在也不支持),因此類模版也不支持重載。而一直以來,C++函數(shù)都是支持重載的,因此函數(shù)模版也就支持重載了??雌饋碇皇且患茏匀坏氖虑椋?進行了一個簡單的總結:
// 例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)
未特化的模版通常也叫做底層基礎模版。
此外,基礎模版一般都是可以特化的。但是特化對于類版本和函數(shù)模版來說有很大的不同,并且這個差異跟我們下面的討論有比較重要的關系。類模版可以偏特化和全特化,而函數(shù)模版只能進行全特化,但是由于函數(shù)模版可以重載,我們通過重載可以獲得和偏特化幾乎相同的效果。下面的代碼說明了這些不同點:
// 續(xù)例1:模版特化
//
// (a)模版針對指針類型的偏特化
template<class T> classX<T> { /...*/ };
// (a)模版針對int類型的全特化
template<> class X<int> {/.../ };
// 重載(b)和(c)的獨立基礎模版
// 需要注意的是,它不是(b)的偏特化,
// 因為在函數(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)在讓我們把討論的焦點放到函數(shù)模版上面,并且思考一下重載的規(guī)則,看看在不同的情況下到底哪個重載函數(shù)被調(diào)用。規(guī)則其實非常簡單,至少從優(yōu)先級上來說,可以分為典型的兩大類:
非模版函數(shù)(譯注:就是普通函數(shù))的優(yōu)先級最高,如果一個非模版函數(shù)可以完全匹配所有的參數(shù)類型,則會被優(yōu)先選擇調(diào)用,即使有適合的函數(shù)模版也可以完全匹配所有的參數(shù)類型(譯注:這里的意思就是說,非函數(shù)模版優(yōu)先級最高,優(yōu)先選中,雖然函數(shù)模版可能也有適合的,但是因為優(yōu)先級低,因此不會沖突)。
如果沒有非函數(shù)模版匹配,那么函數(shù)基礎模版作為第二優(yōu)先級進入候選,但是具體哪個函數(shù)基礎模版被選擇,還需要根據(jù)兩個點來確定,一點比較肯定就是參數(shù)匹配度要是最高的,另一個點就是根據(jù)一套比較晦澀的規(guī)則來判定哪個模版是“最特殊的”(注:這里用“最特殊的”為了和模版特化區(qū)分開來是有點奇怪的,也許是一個不經(jīng)意的誤會吧)。下面我們看下這套晦澀的規(guī)則:
如果很明顯有一個“最特殊的”函數(shù)基礎模版,那么它就被選中了。如果這個基礎模版恰好還有一個專門為所用的參數(shù)類型特化的版本,那么特化版本優(yōu)先被選中,否則基礎模版被選中。
而如果有一個同級別的“最特殊的”函數(shù)基礎模版,那么調(diào)用將會發(fā)生二義性(譯注:編譯器不知道選擇哪個),編譯器無法決定哪個是最合適的,此時程序員需要明確指定選擇哪一個基礎函數(shù)模版以消除二義性。
另外,如果沒有合適的函數(shù)基礎模版可以匹配參數(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ū)或者說陷阱,去看看一些復雜的情況。
為什么不要特化:Dimov/Abrahams的例子
思考下面的代碼:
// 例子2:顯示特化
//
template<class T> // (a) 一個基礎模版
void f( T );
template<class T> // (b) 另一個基礎模版,(a)的重載版本
void f( T* ); // (函數(shù)模版沒有偏特化;可以用重載替代)
template<> // (c) (b)模版的全特化
void f<>(int*);
// ...
int *p;
f( p); // 調(diào)用(c)
例子2里面最后一行選擇調(diào)用的模版可能正是你所期望的,不過,問題是,它為什么是你所期望的結果呢?如果你期望的原因是錯誤,接下來發(fā)生事情可能令你更加驚訝。也許,有人可能會說,我給int指針類型寫了一個專門的特化版本啊,因此這次調(diào)用理所當然就應該是選這個特化版本啊,然而,這正是錯誤的原因。
再考慮下下面的代碼,這個例子是由Peter Dimov和Dave Abrahams提出的:
// 例子3:Dimov 和Abrahams提供的例子
//
template<class T> // (a) 和之前一樣的基礎模版
void f( T );
template<> // (c) 針對(a)的完全特化
void f<>(int*);
template<class T> // (b) 另一個基礎模版,對(a)的重載
void f( T* );
// ...
int *p;
f( p); // 調(diào)用的是(b)!重載解析規(guī)則導致特化版本被忽略了
// 因為有基礎模版優(yōu)先匹配了
??如果你也感到吃驚,恭喜你,你并不是第一個對此感到驚訝的人。很多專家級別的程序員也對此感到驚訝,其實理解這個問題關鍵并不復雜,那就是函數(shù)全特化并不參與重載!
只有基礎模版是可以重載的(當然,也包括非模版函數(shù))。再回顧下前面給出的重載解析規(guī)則,這次我特意高亮了一些關鍵詞:
……
??如果沒有非函數(shù)模版匹配,那么函數(shù)基礎模版作為第二優(yōu)先級進入候選,但是具體哪個函數(shù)基礎模版被選擇,還需要根據(jù)兩個點來確定,一點比較肯定就是參數(shù)匹配度要是最高的,另一個點就是根據(jù)一套比較晦澀的規(guī)則來判定哪個模版是“最特殊的”:
??如果很明顯有一個“最特殊的”函數(shù)基礎模版,那么它就被選中了。而如果這個基礎模版恰好有一個專門為參數(shù)類型特化的版本,那么特化版本優(yōu)先被選中,否則基礎模版被選中。
……等等。重載解析規(guī)則只會先從基本模版(或者非模版函數(shù))里面進行挑選。當某個基礎模版被選中之后,選擇會被鎖定,此時,編譯器會針對這個基本模版進行搜索,檢查是否恰好有一個更合適的全特化模版,如果有的話,那么就會使用這個特化模版。
重要條款
如果你跟我一樣,第一次看到這種情況時,可能會問這種問題:“嗯,我已經(jīng)針對參數(shù)是int指針類型寫了一個專門的特化版本,而且這里剛好是一個參數(shù)是int類型的調(diào)用,恰好匹配啊,那為什么我寫的特化版本沒有被選中呢?”可惜,呃,這個想法是錯誤的:如果這種情況下(譯注:指用int*參數(shù)進行調(diào)用)你想你的特化版本能被選中,其實你把特化模版它改成一個普通函數(shù)就可以做到。
??要理解為什么模版函數(shù)的特化(譯注:這里指模版函數(shù)的全特化,模版函數(shù)沒有偏特化)不參與函數(shù)重載看起來挺怪異,但是一旦解釋清楚了也很簡單。因為這個原因可能跟你想的恰恰相反:如果因為你針對一個特殊的模版寫了一個特化版本,就要改變模版的選擇規(guī)則,這回驚訝的就是標準委員會了(譯注:意思就是標準委員會規(guī)定了特化版本不能改變模版選擇規(guī)則)。在這樣的邏輯或者標準下,如果我們已經(jīng)有辦法讓函數(shù)調(diào)用選擇我們想選擇的版本(譯注:譬如上面提到的用普通函數(shù))(我們做的僅僅是讓他成為一個普通函數(shù),而不是一個特化),這樣也許我們能夠更加深刻的理解為什么特化版本不會影響模版的選擇過程。
??條款1:如果你正在定制一個函數(shù)基礎模版,并且希望有重載版本(在一些特殊情況下有專門的精確匹配版本),建議你使用普通的函數(shù)重載,而不要使用特化。并且,如果你提供了重載的函數(shù)模版,也盡量不要特化它。 但是,你不僅僅是在使用函數(shù)模版,而是在寫一個函數(shù)模版(譯者注:可以認為你是公共庫的作者),那么怎樣才能讓函數(shù)模版的使用者(包括自己),不遇到前面出現(xiàn)的那個問題呢?實際上,你還可以這樣做(參考條款#2):
??條款2:如果你正在寫一個函數(shù)基礎模版,并且希望它成為一個唯一的函數(shù)模版,絕不對它進行特化和重載,那么你可以把這個函數(shù)模版實現(xiàn)為包含一個相同簽名的靜態(tài)函數(shù)的簡單類模版。每個人都可以對它進行特化和偏特化,但是不會對重載解析規(guī)則產(chǎn)生任何影響。
總結
??重載函數(shù)模版是一種不錯的方式,重載解析過程會平等的對待所有的基礎模版,而且整個解析過程和普通函數(shù)重載是類似的,令人感覺很自然,符合期望,所有可見的重載函數(shù)模版都納入重載解析規(guī)則,然后編譯器從里面選出一個最合適的。
??特化函數(shù)模版就是一個不太直觀的事情,首先,你還不能偏特化函數(shù)模版——原因是C++語言標準規(guī)定了!其次,函數(shù)模版特化后的版本還不參與重載。也就是說,任何函數(shù)模版的特化版本都不影響重載解析過程,這種行為可能違背了大多數(shù)人的直覺。然而,如果你寫了一個非模版函數(shù)用來完全替代某函數(shù)模版的特化版本(譯注:函數(shù)簽名一樣,其實把函數(shù)模版的特化版本的模版聲明去掉就變成了普通函數(shù)),這個普通函數(shù)反而會被優(yōu)先選擇,因為根據(jù)C++標準,普通函數(shù)總是應該認為比模版函數(shù)的匹配度要好。
??如果你正在寫一個函數(shù)模版,希望它成為一個不會被特化或者重載的唯一的函數(shù)模版,并且你在一個類模版中實現(xiàn)了這個函數(shù)模版,這可能是一個眾所周知的繞過函數(shù)模版局限性和陷阱(譯注:指函數(shù)模版沒有偏特化和前面遇到重載解析的問題)的好辦法。使用這種方法,程序員可以在類模版的基礎上不受限制的使用偏特化和全特化(顯示特化),并且不會影響到原始的函數(shù)模版的任何期望的動作。這樣就避免了函數(shù)模版的兩個限制,不能偏特化,以及某些情況下因為函數(shù)特化版本不參與重載帶來的令人驚訝的的效應。至此,問題全部解決。如果你正在使用某人寫的老式的函數(shù)模版(沒有使用上面例子4里面使用類模版包裝的方法),并且想寫一個特殊情況下的重載版本,不要特化它,只要寫一個相同函數(shù)簽名的普通函數(shù)就可以了。