程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 模板友元化

模板友元化

編輯:關於VC++

摘要:

如何將一個函數模板的特化聲明為友元呢?標准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中創建一個代理類 並對其友元化。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved