模板友元化

摘要:

    如何将一个函数模板的特化声明为友元呢?标准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)。

表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)。

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

关于作者:

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

阅读更多
想对作者说点什么? 我来说一句

张正友标定法—图片

2012年03月23日 3.2MB 下载

友价源码房产网T6.1模板

2018年05月03日 9.29MB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭