让模板成为友元
作者:herb sutter
出处:cuj:sutter’s mill
--------------------------------------------------------------------------------
假设我们有一个函数模板,对它所操作的对象执行somethingprivate()。 特别地,考虑一下
boost::checked_delete() 函数模板,它delete传给它的对象-- 在此过程中,它调用对象的析构函数:
namespace boost {
template<typename t> void checked_delete( t* x ) {
// ... other stuff ...
delete x;
}
}
现在,假定你想将这个函数模板作用于一个类,问题是,你所需要的操作(这里是析构函数) 碰巧是私有
的:
// example 1: no friends
//
class test {
~test() { } // private!
};
test* t = new test;
boost::checked_delete( t ); // error: test's destructor is private,
// so checked_delete can't call it.
解决方法非常简单:只要让checked_delete() 成为test的友元。
(唯一的其它选择是放弃[封装],让析构函数成为公有)。
我为此写下这篇文章是因为,唉,让处于另外一个namespace中的模板成为友元,说比做容易多了:
l 坏消息: 有两个完全符合标准的方法可以实现它,但没有一个可在目前所有的编译器上工作。
l 好消息: 其中之一除了gcc以外,在我所尝试的每一目前的编译器上的都能工作。
最初的尝试
--------------------------------------------------------------------------------
这里是最初的代码:
stephan born <stephan.born@bneousspeanm.de> 写为:
// example 2: one way to grant friendship (?)
//
class test {
~test() { }
friend void boost::checked_delete( test* x );
};
唉,这个代码不能工作于发贴者的编译器上(vc++6.0)。事实上,它
在很多编译器上的都不行。 要之,例子2中的友元声明:
l 技术上合法,但是依赖于[c++]语言的一个含糊点
l 被许多目前的编译器拒绝,包括一些非常优秀的编译器
l 很容易被修正为不依赖于含糊点,并工作在几乎目前所有的编译器上 (除了gcc)
为什么它是合法的,却又含糊的
当申明友元时,将发生四种情形 (列举于c++标准,clause 14.5.3)。摘要如下:
当申明友元却没有在其中任何地方使用“template”关键字时:
1. 如果,友元的名字看起来像带着显式参数的模板特化版本(比如,name<sometype>),
那么,友元是那个模板的一个显式特化版本
2. 其次,如果,友元的名字被类名或namespace名 (比如,some::name)限定,并且,类或namespace
中包含一个匹配的非模板函数,
那么,友元就是那个函数
3. 再者,如果, 友元的名字被类名或namespace名 (比如,some::name)限定,并且,类或namespace
中包含一个匹配的模板函数(能够推导出恰当的模板参数),
那么,友元就是那个函数模板的一个特化版本
4. 最后,那个名字必须是无[类或namespace名]限定的,并且申明(或重复申明)了一
普通的 (非模板) 函数。
很清楚, #2 和 #4 只匹配于非模板,因此,将模板的特化版本申明为一个友元,我们有两个选择: 写成
满足规则#1,或写成满足规则#3。对应于我们的例子,选择是:
// the original code, legal because it falls into bucket #3
//
friend void boost::checked_delete( test* x );
或
// adding "<test>", legal because it falls into bucket #1
//
friend void boost::checked_delete<test>( test* x );
第一个是第二个的简写形式……但是,只有当名字是有限定的( 这里被“boost::”),并且, 没有相匹
配的非模板函数位于同一作用域空间。 这个友元声明规则的含糊点把人弄晕了╟ 对绝大部分当前的编译
器也是如此!一我能找到至少三个理由以要求避免使用它。
为什么要避免规则 #3
有好些理由要求避免规则#3,即使它在技术上是合法的:
1. 规则#3 并不总能工作。
如上所述,它是最初规则的简写形式,但是只在有类名或namespace名限定,并且其中没有相匹配的非模
板函数时,才起作用。
特别地,如果 namespace 有 ( 或稍后又获得) 一个匹配的非模板函数,会改变选择,因为非模板函数的
出现将导致规则#2抢先了规则#3。 有些微妙和令人惊讶,不是吗?很容易犯错,不是吗?让我们避免这
样的微妙。
2. 规则#3真的很锋利,易碎,并让大多数读你的代码的人感到吃惊。
举例来说, 考虑这个非常微小的变体 -- 我所作的所有改变就是去除限定字“boost”:
// variant: make the name unqualified
//
class test {
~test() { }
friend void checked_delete( test* x ); // ouch: legal, but not what you
}; // want. more about this later.
如果你省略 “boost::”(也就是, 如果调用是无限定的),你将掉入一个完全地不同的规则中(规则#4)
,根本不对函数模板进行匹配,这可一点都不好玩。 赌二十块钱,差不多我们这个美丽行星上每一个人
都会同意我的观点:太令人吃惊了,仅仅省略一个 namespace 名字,就如此的大幅地改变了友元声明的
意义。 让我们避开这个锋利的东西吧。
规则#3锋利、易碎,搞昏了大多数编译器
让我们用规则#1和规则#3,在目前的编译器上进行一个大范围的试验,看看它们是怎么理解的。编译器对
标准的理解和我们一致吗(在我们读了上面这么多之后)?至少最牛的编译器会符合我们的期待吧?否,
还是否(no, and no, respectively)。
让我们先试规则#3:
// example 1 again
//
namespace boost {
template<typename t> void checked_delete( t* x ) {
// ... other stuff ...
delete x;
}
}
class test {
~test() { }
friend void boost::checked_delete( test* x ); // the original code