摘要:
如何將一個函數模板的特化聲明為友元呢?標准C++給你提供了兩種合法的 語法。然而,事實上,對於其中的一種語法,幾乎沒有編譯器對其給予支持;而對於另一種 ,當前所有主流編譯器(除了一款以外)都對其提供了支持。
假設我們有一個函數模 板,可以調用其所操作的對象的SomethingPrivate()方法。特別地,考慮 boost::checked_delete()函數模板,它用以刪除指定的對象——在它的實現中, 會調用該對象的析構函數:
namespace boost {
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
}
現在,假設你想要在一個類中使用該函數模板,則該類中只 有一個私有的方法(析構函數):
class Test {
~Test() { } // 私有的!
};
Test* t = new Test;
boost::checked_delete( t ); // 錯誤:
// Test 的析構函數是私有的,
// 因此checked_delete不能調用它 。
解決方案很簡單:只要令checked_delete()成為Test的友元即可。(其 它的方法都需要Test提供公共的析構函數)如何才能實現這個容易的解決方案呢?事實上, C++標准提供了2種方法來合法又便捷的實現它。
本文將提供一個現實的檢驗:在某個 命名空間中,友元化一個模板――說起來容易做起來難!(現實的編譯器並未對標准有完好 的支持。)
總體來說,我有以下幾條好消息和壞消息:
好消息:存在兩種對 其支持得很好的符合標准的方法,它們的語法很平凡且不會使人困惑。
壞消息:沒有 哪一種編譯器對這兩種標准語法提供完全的支持。甚至一些最健壯且幾乎完全實現了C++標准 的編譯器都不能對它們兩個或其中之一提供完好的支持。
好消息(重復):我用來測 試它的當前的每一個編譯器(除了gcc以外)都至少對二者之一有完好的支持。
讓我 們再多花點兒時間來看看吧。
最初的嘗試
本文所述曾經被Stephan Born 在 Usenet中作為一個問題提出,他想要做如上的事情。他的問題是,當他嘗試將 boost::checked_delete()的一個特化聲明為Test類的友元時,代碼不能被他使用的 Microsoft Visual C++ 6.0編譯器所接受。
下邊是他的源代碼:
//例1: 授權給友元的方法
class Test {
~Test() { }
friend void boost::checked_delete( Test* x );
};
事實上,上述代碼不僅不能通過上 邊所說的編譯器的編譯,而且不能通過幾乎所有的編譯器。簡單的說,例1的友元聲明:
是符合標准的,但卻依賴語言的晦澀之處。
是被當前大多數編譯器所拒絕的 ,包括一些很好的編譯器。
是容易被修復成不依賴於此晦澀之處的,而且可以通過當 前的所有編譯器,除了gcc。
我將要深入研究解釋C++語言提供給你用來聲明友元的四 種方法。那是容易的。我也會給你看一些現實中的編譯器處理它的有趣的東西,並提出一個 方針來實現最便捷的代碼,來結束本文。
為什麼合法但卻晦澀
C++標准的第 14.5.3條列舉了四條聲明友元的規則,歸結如下:
1、如果該友元的名字是一個具有 確切的模板參數的特化了的模板名字(例如:Name<SomeType>)
則,友元就是 此模板的特化。
2、否則,如果該友元在某個類或者命名空間(例如:Some::Name) 中,而且該類或者命名空間包含一個匹配的非模板函數,
則,友元就是該函數。
3、否則,如果該友元是在某個類或者命名空間(例如:Some::Name)中的,而且該 類或者命名空間包含一個匹配的模板函數(具有適當的模板參數)
則,友元就是該函 數模板的特化。
4、否則,該友元必須在全局命名空間內(unqualified。譯者:我將 unqualified理解為處於全局命名空間,不知對否。),而且聲明為(或重新聲明)一個常規 函數(非模板)。
很明顯,#2和#4只匹配非模板函數,因此我們有2個選擇來將某個 模板的特化聲明為友元:寫成#1的形式,或者寫成#3的形式。在我們的例子中,可選擇如下 :
//源代碼,合法,因為它符合#3的形式
friend void boost::checked_delete( Test* x );
或者
// 增加了 "<Test>",合法,
// 因為5它符合#1的形式
friend void boost::checked_delete<Test>( Test* x );
前者是後者的簡化形式...但 只有在該名字處於某一作用域(此例為boost::)中,而且其作用域中必須不存在與其匹配的 非模板函數。兩者都是合法的,但是前者運用了友元聲明規則中的晦澀之處,它會令使用它 的人感到困惑——對當前的大多數編譯器來說!——下邊闡述了為何 要求避免使用它的三個原因。
為什麼避免#3
有以下幾個原因,即使其技術是 合法的:
1、#3並不總能正常工作。
如上所述,它是一個以<>清楚地命 名了模板參數的簡化形式,但是該形式只有在——被某個類或者命名空間限定, 而且其作用域中不存在與其相匹配的非模板函數——時,才正常工作。 特別地, 如果命名空間中有一個(尤其是以後才加入的!)一個匹配的非模板函數,那麼該命名將被 覆蓋——因為存在一個非模板的函數意味著#2優先於#3。看起來有點兒小聰明似 的,卻很令人驚訝吧?很容易出錯吧?讓我們避免這樣的小聰明吧。
2、#3處於一種 顛簸(edgy)的狀態,很容易被閱讀你代碼的人破壞(fragile),而且令她感到驚訝。
例如,考慮如下細微的變化――我所做的只是去掉了限定域boost::。
// 變化: 去掉該名字的限定域,
// 這意味著產生了很大的變化。
class Test {
~Test() { }
friend void checked_delete( Test* x );
};
如果你 忽略了boost::(例如,如果該調用是無限定域的),那麼你其實是使用了#4,它根本就不包 含函數模板,盡管它看起來優雅且簡練。我敢和你用打賭買根"老高太太糖葫蘆" (譯者:donuts,面包圈,不可以隨便譯麼?^_^),我認為我們這個美麗行星上的每個人都 會同意我的看法——只忽略了命名空間的名字卻如此劇烈的改變了友元聲明的含 義——這是非常不合理的。讓我們必避免這種顛簸的構造吧。
3、#3處於 一種顛簸(edgy)的狀態,很容易被分析你代碼的編譯器破壞(fragile),而且令她感到驚訝 。
讓我們分別用#1和#3來看看現在的編譯器都是怎麼想的吧。編譯器對C++標准的 理解會和我們一樣麼?是不是至少會有些最健壯的編譯器會如我們所期待的那樣工作呢?不 ,不是這樣的。
讓我們首先試試#3吧:
// 再來看看例1
namespace boost {
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
}
class Test {
~Test() { }
friend void boost::checked_delete( Test* x ); // 原始代碼
};
int main() {
boost::checked_delete( new Test );
}
在你自己的編譯器上試試看,比較我們的結果。如果你曾經看過電視節目"家族分歧 "(Family Feud),你現在可能會想象得到Richard Dawsond的名言了:"Survey Saaaaays"(譯者:橫向比較?原文就是那麼多個a呀:)(見表1)。
這種情況下,橫 向比較的結果說明了此語法並沒有被現在的編譯器所公認。順便說一句,令我們很驚訝的是 Comeau, EDG, Intel 編譯器都承認了這種語法,這是因為它們都是基於EDG C++來實現的。 在被測試的5種不同的C++語言實現中,有三種不能支持這個版本(gcc, Metrowerks, Microsoft),另外兩種支持(Borland, EDG)。
讓我們接著來試試C++標准所支持的 另一種方法吧,#1:
// 例2:聲明友元的另一個方法
namespace boost {
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
}
class Test {
~Test() { }
friend void boost::checked_delete<>( Test* x );
};
int main() {
boost::checked_delete( new Test );
}
或者,等價地, 我們清晰地聲明:
friend void boost::checked_delete<Test>( Test* x );
無論哪一種,對上邊的編譯器測試的橫向比較結果說明了它們被支持得更好( 見 表2)。
#1應該是更安全 的――例2得到當前的編譯器(除了gcc)和每個老式的編譯器(除了MSVC++6.0)很好地支持 ;
旁白:是命名空間引起的混淆
注意,如果我們要友元化的函數模板存在於 同一個命名空間中,那麼我們可以在現今幾乎所有的編譯器上正確的使用它:
// 例3:如果checked_delete不在一個命名空間中...
// 不再在 boost:: 中
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
class Test {
// 不再需要 "boost"
friend void checked_delete<Test>( Test* x );
};
int main() {
checked_delete( new Test );
}
橫向比 較...(見 表3)。
因為,問題 ——大多數編譯器上不能處理例1――產生於在另一個命名空間中明確地聲明了某 個函數模板的特化。(喝倒彩三聲?:)微軟的Visual C++ 6.0 編譯器甚至不能處理最簡單 的情況。
兩種錯誤的答案(Non-Workarounds)
當這個問題在Usenet被提出時, 一些人的回復中建議用一個using聲明(或者等價地using指示),去掉友元聲明的作用域限 定:
namespace boost {
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
}
using boost::checked_delete;
class Test {
~Test() { }
// 沒有模板特化!
friend void checked_delete( Test* x );
};
上邊的友元聲明又落入了#4的形式:"4.否則,友元的名字必須不被冠 以作用域修飾,而是聲明為一個常規函數(非模板)。"這實際上是在全局命名空間中 聲明了一個新的常規非模板函數::checked_delete(Test*)。
如果你試試上邊的代碼 ,上述數編譯中的大多數器都會拒絕它,並提示checked_delete()沒有被定義;而且它們全 部都會拒絕讓你在boost::checked_delete()模板中以友元的身份去調用類的私有成員。
最後,一位專家建議把它稍稍改一下——使用"using"也是用 模板語法"<>":
namespace boost {
template<typename T> void checked_delete( T* x ) {
// ... 其它代碼 ...
delete x;
}
}
using boost::checked_delete;
class Test {
~Test() { }
friend void checked_delete<>( Test* x ); //合法麼?
};
上邊不是合法的C++代碼——C++標准沒有明確指 出這是合法的。在標准委員會中,曾經有一過一次公開的討論——以決定該用法 是否合法,存在一個觀點認為它應該是非法的,因為事實上所有我測試過的當前編譯器都拒 絕它。為什麼人們認為它不能是合法的呢?為了保持一致性,因為using的存在是為了令名字 使用起來更加容易——調用函數/在變量或參數聲明中使用類型名。聲明有所不同 的是:正如你必須在模板的原始作用域中聲明該模板的一個特化一樣,(你不能在另一個命 名空間中通過"using"來達到這一目的),你只能將一個模板的特化聲明為 ——冠以該模板作用域的——友元(而不能通過"using"來 做到這一點)。
總結
為了友元化一個函數模板的特化,應該選擇如下2種語法 之一:
// 來自例1
friend void boost::checked_delete ( Test* x );
// 來自例2:增加<>或<Test>
friend void boost::checked_delete<>( Test* x );
本文演示了——不像例 2所示,寫上"<>"或"<Test>"的代碼所產生的 ——嚴重的移植性問題。
方針:說明白你到底想要什麼。 (Guideline:Say what you mean, be explicit.)
當你友元化一個函數模板的特化 時,應該總是清楚地冠以模板的語法,至少加上"<>"。例如:
namespace boost {
template<typename T> void checked_delete( T* x );
}
class Test {
friend void boost::checked_delete ( Test* x ); // 不好
friend void boost::checked_delete<>( Test* x ); // 好
};
如果你的編譯器不 支持這兩種聲明友元的合法語法的話,你就要把必要的函數聲明為公共的了――不過,應該 加上一條注釋以說明原因,並提醒自己一旦編譯器升級了的話,便應嘗試將這些函數聲明改 回成私有的。
承謝
感謝John Potter對本文草稿的審校。
注釋
[1] 有其它的實現方式,但卻笨拙。例如:可以在命名空間boost中創建一個代理類 並對其友元化。