Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1568799
  • 博文数量: 884
  • 博客积分: 52280
  • 博客等级: 大将
  • 技术积分: 13060
  • 用 户 组: 普通用户
  • 注册时间: 2008-08-06 09:46
文章分类

全部博文(884)

文章存档

2008年(884)

我的朋友

分类: C/C++

2008-08-06 09:52:16

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

namespace boost {

  template 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

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

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

    则,友元就是该函数模板的特化。
  • 4、否则,该友元必须在全局命名空间内(unqualified。译者:我将unqualified理解为处于全局命名空间,不知对否。),而且声明为(或重新声明)一个常规函数(非模板)。
很明显,#2和#4只匹配非模板函数,因此我们有2个选择来将某个模板的特化声明为友元:写成#1的形式,或者写成#3的形式。在我们的例子中,可选择如下:

//源代码,合法,因为它符合#3的形式

friend void boost::checked_delete( Test* x );      
或者
// 增加了"",合法,

// 因为5它符合#1的形式

friend void boost::checked_delete( 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 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 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* x );      
无论哪一种,对上边的编译器测试的横向比较结果说明了它们被支持得更好(见 表2)。



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

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

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

// 不再在 boost:: 中

template void checked_delete( T* x ) {

  // ... 其它代码 ...

  delete x;

}



class Test {

  // 不再需要 "boost"

  friend void checked_delete( Test* x );

};



int main() {

  checked_delete( new Test );

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



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

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

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

  template 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 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:增加<>或

  friend void boost::checked_delete<>( Test* x );      
本文演示了——不像例2所示,写上"<>"或""的代码所产生的——严重的移植性问题。

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

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

  template 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中创建一个代理类并对其友元化。

关于作者:

Herb Sutter(<):ISO C 标准委员会的成员之一,著有经典名著《Exceptional C 》和《More Exceptional C 》, 并作为C 研究协会(The C Seminar)(<)的讲师。另外,Herb Sutter从事独立的写作和咨询工作,他也是C 社团和微软公司的联系枢纽。
本文出处:CUJ专家论坛,January 2003
原文题目:Sutter''s Mill: Befriending Templates
原文地址:http://www.cuj.com/experts/2101/sutter.htm?topic=experts

译者信息:


个人主页:
电子邮件地址:kesongemini@vip.163.com
本文仅供学习之用,无任何商业利益 下载本文示例代码
阅读(177) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~