為什麼我們不建議使用函式模板具體化
Overloading與 Specialization的重要不同
你有過使用了函式具體化模板卻沒有得到預期結果的經歷嗎?
本文部分譯自[Why Not Specialize Function Templates?]。具有一定英語閱讀能力的讀者強烈和強烈建議跳過此部落格直接閱讀原文。(http://www.gotw.ca/publications/mill17.htm#1)
眾所周知,C++中存在著兩類模板——函式模板與類模板。在過載與具體化方面它們存在著巨大的不同。
過載
值得引起注意的是,過載是隻有函式才有的特性,而對於類模板來說它是沒有意義的。這是顯然的,因為過載是指函式名相同而函式簽名不同。
// Example 1: Class vs. function template, and overloading
//
// A class template
template<class T> class X { /*...*/ }; // (a)
// A function template with two overloads
template<class T> void f( T ); // (b)
template<class T> void f( int, T, double ); // (c)
像a,b,c這樣沒有具體化(specialized)的模板被稱為基模板
(base template).
具體化
談到具體化,函式模板與類模板的差異就顯得更大了.這裡有一句相當重要的概念,我強烈建議讀者們把這句話徹底理解了再往下閱讀:
類模板可以被完全具體化
2
(fully specialized)或者部分具體化(partially specialized)3
;
函式模板只能被完全具體化,但它可以通過函式過載達到與部分具體化相似的效果.
不妨通過下面這段程式碼更好地理解上面的話:
// Example 1, continued: Specializing templates
//
// A class template
template <class T> class X { /*...*/ }; // (a)
// A function template with two overloads
template<class T> void f( T ); // (b)
template<class T> void f( int, T, double ); // (c)
// A partial specialization of (a) for pointer types
template<class T> class X<T*> { /*...*/ }; //a1
// A full specialization of (a) for int
template<> class X<int> { /*...*/ }; //a2
// A separate base template that overloads (b) and (c)
// -- NOT a partial specialization of (b), because
// there's no such thing as a partial specialization
// of a function template!
template<class T> void f( T* ); // (d)
// A full specialization of (b) for int
template<> void f<int>( int ); // (e)
// A plain old function that happens to overload with
// (b), (c), and (d) -- but not (e), which we'll
// discuss in a moment
void f( double ); // (f)
再強調一下,a1是基模板a的部分具體化,a2是基模板a的完全具體化.
而與此不同的是,d其實是對基模板b,c的過載,某種程度上可以看作是並列的關係.因為d本質上也是一個基模板而並非基模板的具體化.
那麼讀者可能會問,那e呢?這正是我們稍後要討論的重點.事實上,e並不是基模板,當然也不會被過載.
函式模板具體化不會被過載!
但在此之前,先讓我們瞭解一下在不同的情況下使用的究竟是哪個函式模板.其實這一規則可以簡單的總結為一個兩級系統:
非模板函式處於第一優先順序.在函式引數具有相同的匹配度的情況下,非模板函式優先於任何模板函式.
在沒有第一優先順序函式的情況下,也就是沒有匹配的非模板函式的情況下,第二優先順序的函式將得到考慮.第二優先順序的函式是指基模板的模板函式.至於哪一個基模板函式將被選擇,將根據我們熟悉的最佳匹配原則.這可以分為三種情況:
if (確實存在一個唯一的最佳匹配基模板函式)
{
if(該基模板恰好存在具體化而且該具體化適用於本次引數呼叫)使用該具體化;
else 直接使用該基模板進行例項化;
}
else if(同時存在多個最佳匹配基模板函式)
{
編譯器報出"ambiguous"矛盾;
return GG;
}
else
{
沒有基模板函式與引數相匹配;
這是一個"bad call",這就需要"fix code";
}
具體例子–不建議使用函式模板具體化的原因所在
先看下面一段程式碼
// Example 2: Explicit specialization
//
template<class T> // (a) a base template
void f( T );
template<class T> // (b) a second base template, overloads (a)
void f( T* ); // (function templates can't be partially
// specialized; they overload instead)
template<> // (c) explicit specialization of (b)
void f<>(int*);
// ...
int *p;
f( p ); // calls (c)
我們可能很自然的認為f(p)
呼叫的是c是很合理的一件事情,畢竟這裡好像就是它”最佳匹配”,然而倉鼠發現事情並沒有那麼單純.再看下面的一段程式碼
// Example 3: The Dimov/Abrahams Example
//
template<class T> // (a) same old base template as before
void f( T );
template<> // (c) explicit specialization, this time of (a)
void f<>(int*);
template<class T> // (b) a second base template, overloads (a)
void f( T* );
// ...
int *p;
f( p ); // calls (b)! overload resolution ignores
// specializations and operates on the base
// function templates only
這一次,f(p)
呼叫的實際上卻是b.
為什麼呢?我們可以返回頭去看函式模板的使用規則.
由於函式模板沒有部分具體化只有過載,所以a和b是一對並不相同的基模板.又由於c的特殊構造使得它既可以作為a的完全具體化,也可以作為b的完全具體化,所以編譯器將按它宣告的位置來決定c究竟是屬於哪一個基模板的具體化.
很顯然,在例2中它充當了b的具體化,而在例3中充當了a的具體化.
因此按照前面所說的函式模板的選用規則
在例2和例3中,首先沒有非模板函式,因此編譯器轉入下一個步驟:尋找最佳匹配的基模板函式. 由於顯然都是b基模板更像是一個”最佳匹配”,所以這兩個例子中編譯器都選擇了b基模板. 自然地,例2中具有c這一具體化模板與引數更加匹配,所以就呼叫了b基模板的具體化c. 而反觀例3,這一次b基模板可沒有具體化的模板了,因此很自然地直接使用了c來進行例項化.
總結
可能有些讀者為函式模板沒有部分具體化而只能使用過載來達到類似的效果感到莫名其妙.事實上根本原因來自標準委員會所作出的規定.而為什麼具體化也不參與函式過載,而必須依附於基模板而存在,這也是標準委員會所作出的規定.(事實上C++標準委員會也討論過並且可能還在繼續討論要不要允許函式進行部分具體化)
綜上所述,為了避免上面所舉例子可能帶來的負面作用,我們給出兩點建議:
Moral_1:
優先選擇過載非模板函式;
Moral_2:
確實想要使用函式具體化的話,那麼像下面這樣把它封裝到類或結構體(靜態成員)中,這樣就能進行部分具體化和完全具體化了,並且不影響類外的函式過載:
// Example 4: Illustrating Moral #2
//
template<class T>
struct FImpl;
template<class T>
void f( T t ) { FImpl<T>::f( t ); } // users, don't touch this!
template<class T>
struct FImpl
{
static void f( T t ); // users, go ahead and specialize this
};