模板友元化

       在C++中,重载二元运算符时往往需要使用友元,而在类模版中,重载函数也需要使用函数模版,这件事就变得很麻烦。LZ在linux平台下使用GCC编译器,出现各种问题,就是不能成功将函数模版实例化。
      下面的文章提供的方法没有成功。并且经过试验,发现在函数模版实例化时需要在前面加上template<>,即:

函数模版申明:template<typename T> void func( T* x );

实例化:template<>void func<int>( int* x );

=================================================================================

假设我们有一个函数模板,可以调用其所操作的对象的SomethingPrivate()方法。特别地,考虑boost::checked_delete()函数模板,它用以删除指定的对象——在它的实现中,会调用该对象的析构函数:

1.namespace boost {
2.template<typename T> void checked_delete( T* x ) {
3.// ... 其它代码  ...
4.delete x;
5.}
6.}

现在,假设你想要在一个类中使用该函数模板,则该类中只有一个私有的方法(析构函数):

1.class Test {
2.~Test() { }               // 私有的!
3.};
4. 
5.Test* t = new Test;
6.boost::checked_delete( t ); // 错误:
7.// Test 的析构函数是私有的,
8.// 因此checked_delete不能调用它。

解决方案很简单:只要令checked_delete()成为Test的友元即可。(其它的方法都需要Test提供公共的析构函数)如何才能实现这个容易的解决方案呢?事实上, C++标准提供了2种方法来合法又便捷的实现它。

本文将提供一个现实的检验:在某个命名空间中,友元化一个模板――说起来容易做起来难!(现实的编译器并未对标准有完好的支持。)

总体来说,我有以下几条好消息和坏消息:

好消息:存在两种对其支持得很好的符合标准的方法,它们的语法很平凡且不会使人困惑。

坏消息:没有哪一种编译器对这两种标准语法提供完全的支持。甚至一些最健壮且几乎完全实现了C++标准的编译器都不能对它们两个或其中之一提供完好的支持。

好消息(重复):我用来测试它的当前的每一个编译器(除了gcc以外)都至少对二者之一有完好的支持。

让我们再多花点儿时间来看看吧。 

 最初的尝试

本文所述曾经被Stephan Born 在Usenet中作为一个问题提出,他想要做如上的事情。他的问题是,当他尝试将boost::checked_delete()的一个特化声明为Test类的友元时,代码不能被他使用的Microsoft Visual C++ 6.0编译器所接受。

下边是他的源代码:

1.//例1:授权给友元的方法
2.class Test {
3.~Test() { }
4.friend void boost::checked_delete( Test* x );
5.};

事实上,上述代码不仅不能通过上边所说的编译器的编译,而且不能通过几乎所有的编译器。简单的说,例1的友元声明:

是符合标准的,但却依赖语言的晦涩之处。

是被当前大多数编译器所拒绝的,包括一些很好的编译器。

是容易被修复成不依赖于此晦涩之处的,而且可以通过当前的所有编译器,除了gcc。

我将要深入研究解释C++语言提供给你用来声明友元的四种方法。那是容易的。我也会给你看一些现实中的编译器处理它的有趣的东西,并提出一个方针来实现最便捷的代码,来结束本文。 

为什么合法但却晦涩

C++标准的第14.5.3条列举了四条声明友元的规则,归结如下:

1、如果该友元的名字是一个具有确切的模板参数的特化了的模板名字(例如:Name<SomeType>)则,友元就是此模板的特化。

2、否则,如果该友元在某个类或者命名空间(例如:Some::Name)中,而且该类或者命名空间包含一个匹配的非模板函数,则,友元就是该函数。

3、否则,如果该友元是在某个类或者命名空间(例如:Some::Name)中的,而且该类或者命名空间包含一个匹配的模板函数(具有适当的模板参数)则,友元就是该函数模板的特化。

4、否则,该友元必须在全局命名空间内(unqualified。译者:我将unqualified理解为处于全局命名空间,不知对否。),而且声明为(或重新声明)一个常规函数(非模板)。

很明显,#2和#4只匹配非模板函数,因此我们有2个选择来将某个模板的特化声明为友元:写成#1的形式,或者写成#3的形式。在我们的例子中,可选择如下:

1.//源代码,合法,因为它符合#3的形式
2.friend void boost::checked_delete( Test* x );

或者

1.// 增加了"<Test>",合法,
2.// 因为5它符合#1的形式
3.friend void boost::checked_delete<Test>( Test* x );

前者是后者的简化形式...但只有在该名字处于某一作用域(此例为boost::)中,而且其作用域中必须不存在与其匹配的非模板函数。两者都是合法的,但是前者运用了友元声明规则中的晦涩之处,它会令使用它的人感到困惑——对当前的大多数编译器来说!——下边阐述了为何要求避免使用它的三个原因。 

 为什么避免#3

有以下几个原因,即使其技术是合法的:

1、#3并不总能正常工作。

如上所述,它是一个以<>清楚地命名了模板参数的简化形式,但是该形式只有在——被某个类或者命名空间限定,而且其作用域中不存在与其相匹配的非模板函数——时,才正常工作。 特别地,如果命名空间中有一个(尤其是以后才加入的!)一个匹配的非模板函数,那么该命名将被覆盖——因为存在一个非模板的函数意味着#2优先于#3。看起来有点儿小聪明似的,却很令人惊讶吧?很容易出错吧?让我们避免这样的小聪明吧。

2、#3处于一种颠簸(edgy)的状态,很容易被阅读你代码的人破坏(fragile),而且令她感到惊讶。

例如,考虑如下细微的变化――我所做的只是去掉了限定域boost::

1.// 变化:去掉该名字的限定域,
2.// 这意味着产生了很大的变化。
3.class Test {
4.~Test() { }
5.friend void checked_delete( Test* x );
6.};

如果你忽略了boost::(例如,如果该调用是无限定域的),那么你其实是使用了#4,它根本就不包含函数模板,尽管它看起来优雅且简练。我敢和你用打赌买根"老高太太糖葫芦"(译者:donuts,面包圈,不可以随便译么?^_^),我认为我们这个美丽行星上的每个人都会同意我的看法——只忽略了命名空间的名字却如此剧烈的改变了友元声明的含义——这是非常不合理的。让我们必避免这种颠簸的构造吧。

3、#3处于一种颠簸(edgy)的状态,很容易被分析你代码的编译器破坏(fragile),而且令她感到惊讶。

让我们分别用#1和#3来看看现在的编译器都是怎么想的吧。编译器对C++标准的理解会和我们一样么?是不是至少会有些最健壮的编译器会如我们所期待的那样工作呢?不,不是这样的。

让我们首先试试#3吧:

01.// 再来看看例1
02.namespace boost {
03.template<typename T> void checked_delete( T* x ) {
04.// ... 其它代码 ...
05.delete x;
06.}
07.}
08.class Test {
09.~Test() { }
10.friend void boost::checked_delete( Test* x ); // 原始代码
11.};
12. 
13.int main() {
14.boost::checked_delete( new Test );
15.}

在你自己的编译器上试试看,比较我们的结果。如果你曾经看过电视节目"家族分歧"(Family Feud),你现在可能会想象得到Richard Dawsond的名言了:"Survey Saaaaays"(译者:横向比较?原文就是那么多个a呀:)(见表1)。 

 

这种情况下,横向比较的结果说明了此语法并没有被现在的编译器所公认。顺便说一句,令我们很惊讶的是Comeau, EDG, Intel 编译器都承认了这种语法,这是因为它们都是基于EDG C++来实现的。在被测试的5种不同的C++语言实现中,有三种不能支持这个版本(gcc, Metrowerks, Microsoft),另外两种支持(Borland, EDG)。

让我们接着来试试C++标准所支持的另一种方法吧,#1:

01.// 例2:声明友元的另一个方法
02.namespace boost {
03.template<typename T> void checked_delete( T* x ) {
04.// ... 其它代码 ...
05.delete x;
06.}
07.}
08.class Test {
09.~Test() { }
10.friend void boost::checked_delete<>( Test* x );
11.};
12. 
13.int main() {
14.boost::checked_delete( new Test );
15.}

或者,等价地,我们清晰地声明:

1.friend void boost::checked_delete<Test>( Test* x );

无论哪一种,对上边的编译器测试的横向比较结果说明了它们被支持得更好(见 表2)。 

#1应该是更安全的――例2得到当前的编译器(除了gcc)和每个老式的编译器(除了MSVC++6.0)很好地支持; 

 旁白:是命名空间引起的混淆

注意,如果我们要友元化的函数模板存在于同一个命名空间中,那么我们可以在现今几乎所有的编译器上正确的使用它:

01.// 例3:如果checked_delete不在一个命名空间中...
02.// 不再在 boost:: 中
03.template<typename T> void checked_delete( T* x ) {
04.// ... 其它代码 ...
05.delete x;
06.}
07. 
08.class Test {
09.// 不再需要 "boost"
10.friend void checked_delete<Test>( Test* x );
11.};
12. 
13.int main() {
14.checked_delete( new Test );
15.}

横向比较...(见 表3)。 

 

因为,问题——大多数编译器上不能处理例1――产生于在另一个命名空间中明确地声明了某个函数模板的特化。(喝倒彩三声?:)微软的Visual C++ 6.0 编译器甚至不能处理最简单的情况。 

 两种错误的答案(Non-Workarounds)

当这个问题在Usenet被提出时,一些人的回复中建议用一个using声明(或者等价地using指示),去掉友元声明的作用域限定:

01.namespace boost {
02.template<typename T> void checked_delete( T* x ) {
03.// ... 其它代码 ...
04.delete x;
05.}
06.}
07. 
08.using boost::checked_delete;
09. 
10.class Test {
11.~Test() { }
12. 
13.// 没有模板特化!
14.friend void checked_delete( Test* x );
15.};

上边的友元声明又落入了#4的形式:"4.否则,友元的名字必须不被冠以作用域修饰,而是声明为一个常规函数(非模板)。"这实际上是在全局命名空间中声明了一个新的常规非模板函数::checked_delete(Test*)

如果你试试上边的代码,上述数编译中的大多数器都会拒绝它,并提示checked_delete()没有被定义;而且它们全部都会拒绝让你在boost::checked_delete()模板中以友元的身份去调用类的私有成员。

最后,一位专家建议把它稍稍改一下——使用"using"也是用模板语法"<>":

01.namespace boost {
02.template<typename T> void checked_delete( T* x ) {
03.// ... 其它代码 ...
04.delete x;
05.}
06.}
07. 
08.using boost::checked_delete;
09. 
10.class Test {
11.~Test() { }
12.friend void checked_delete<>( Test* x ); //合法么?
13.};

上边不是合法的C++代码——C++标准没有明确指出这是合法的。在标准委员会中,曾经有一过一次公开的讨论——以决定该用法是否合法,存在一个观点认为它应该是非法的,因为事实上所有我测试过的当前编译器都拒绝它。为什么人们认为它不能是合法的呢?为了保持一致性,因为using的存在是为了令名字使用起来更加容易——调用函数/在变量或参数声明中使用类型名。声明有所不同的是:正如你必须在模板的原始作用域中声明该模板的一个特化一样,(你不能在另一个命名空间中通过"using"来达到这一目的),你只能将一个模板的特化声明为——冠以该模板作用域的——友元(而不能通过"using"来做到这一点)。 

 总结

为了友元化一个函数模板的特化,应该选择如下2种语法之一:

1.// 来自例1
2.friend void boost::checked_delete ( Test* x );
3. 
4.// 来自例2:增加<>或<Test>
5.friend void boost::checked_delete<>( Test* x );

本文演示了——不像例2所示,写上"<>"或"<Test>"的代码所产生的——严重的移植性问题。

方针:说明白你到底想要什么。(Guideline:Say what you mean, be explicit.)

当你友元化一个函数模板的特化时,应该总是清楚地冠以模板的语法,至少加上"<>"。例如:

1.namespace boost {
2.template<typename T> void checked_delete( T* x );
3.}
4.class Test {
5.friend void boost::checked_delete ( Test* x ); // 不好
6.friend void boost::checked_delete<>( Test* x ); // 好
7.};

如果你的编译器不支持这两种声明友元的合法语法的话,你就要把必要的函数声明为公共的了――不过,应该加上一条注释以说明原因,并提醒自己一旦编译器升级了的话,便应尝试将这些函数声明改回成私有的。 

 承谢

感谢John Potter对本文草稿的审校。 

 注释

[1] 有其它的实现方式,但却笨拙。例如:可以在命名空间boost中创建一个代理类并对其友元化。 

 关于作者:

Herb Sutter(<www.gotw.ca>):ISO C++ 标准委员会的成员之一,著有经典名著《Exceptional C++》和《More Exceptional C++》, 并作为C++研究协会(The C++ Seminar)(<www.gotw.ca/cpp_seminar>)的讲师。另外,Herb Sutter从事独立的写作和咨询工作,他也是C++社团和微软公司的联系枢纽。

本文出处:CUJ专家论坛,January 2003

原文题目:Sutter''s Mill: Befriending Templates

原文地址:http://www.cuj.com/experts/2101/sutter.htm?topic=experts 

 译者信息:

个人主页:http://kesongemini.diy.163.com

电子邮件地址:kesongemini@vip.163.com

本文仅供学习之用,无任何商业利益


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值