让模板成为友元(Befriending Templates)
by
2007-03-07 09:13:17
作者:Herb Sutter
出处:CUJ:Sutter’s Mill
--------------------------------------------------------------------------------
假设我们有一个函数模板,对它所操作的对象执行SomethingPrivate()。 特别地,考虑一下
boost::checked_delete() 函数模板,它delete传给它的对象-- 在此过程中,它调用对象的析构函数:
namespace boost {
template 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 写为:
// 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),
那么,友元是那个模板的一个显式特化版本
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 "", legal because it falls into bucket #1
//
friend void boost::checked_delete( 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 void checked_delete( T* x ) {
// ... other stuff ...
delete x;
}
}
class Test {
~Test() { }
friend void boost::checked_delete( Test* x ); // the original code
};
int main() {
boost::checked_delete( new Test );
}
用你的编译器编译上面的代码,然后与我们的结果相比较。 如果你曾经看过电视节目“Family Feud”,
你能浮现出Richard Dawson的声音:“Survey saaaaays”。(见表1)
Table 1: The results of compiling Example 1 on various compilers
Compiler Result Error Message
Borland 5.5 OK
Comeau 4.3.0.1 OK
EDG 3.0.1 OK
Intel 6.0.1 OK
gcc 2.95.3 Error `boost::checked_delete(Test *)' should have been declared inside
`boost'
gcc 3.1.1 Error `void boost::checked_delete(Test*)' should have been declared inside
`boost'
gcc 3.2 Error `void boost::checked_delete(Test*)' should have been declared inside `boost'
Metrowerks 8.2 Error friend void boost::checked_delete( Test* x ); name has not been
declared in namespace/class
MS VC++ 6.0 Error nonexistent function 慴oost::checked_delete' specified as friend
MS VC++ 7.0 OK
MS VC++ 7.1 beta Error 慴oost::checked_delete' : not a function
对这个用例,测试结果表明,这个语法并不被目前的编译器良好接受。 顺便提一句,Comeau、EDG、
Intel的编译器都通过,这并不奇怪,因为它们全部基于EDG C++的实现的;在所测试的五种不同的C++语
言实现中,有三个版本不接受 (gcc、Metrowerks、Microsoft),两个可以(Borland、EDG)。
我们试一下另外一个标准兼容的方法,测试规则#1:
// Example 2: The other way to declare friendship
//
namespace boost {
template void checked_delete( T* x ) {
// ... other stuff ...
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 );
随便哪种方式,当用我们的编译器尝试时(we twist our compilers' tails),结果表明它被支持得好
多了。 (见表2)
Table 2: The results of compiling Example 2 on various compilers
Compiler Result Error Message
Borland 5.5 OK
Comeau 4.3.0.1 OK
EDG 3.0.1 OK
Intel 6.0.1 OK
gcc 2.95.3 Error `boost::checked_delete(Test *)' should have been declared inside
`boost'
gcc 3.1.1 Error `void boost::checked_delete(Test*)' should have been declared inside
`boost'
gcc 3.2 Error `void boost::checked_delete(Test*)' should have been declared inside `boost'
Metrowerks 8.2 OK
MS VC++ 6.0 Error nonexistent function 慴oost::checked_delete' specified as friend
MS VC++ 7.0 OK
MS VC++ 7.1 beta OK
规则#1有把握多了--例2工作于除gcc外的所有当前版本编译器和除Microsoft Visual C++ 6.0外的所有
老版本编译器上。
旁白: 是“Namespace”使它们困惑了
--------------------------------------------------------------------------------
注意:如果我们正试图使其成为友元的函数模板不位于不同的命名空间中的话,那么我们今天就可以在几
乎所有这些编译器上正确地使规则#1了:
// Example 3: If only checked_delete
// weren't in a namespace...
//
// No longer in boost::
template void checked_delete( T* x ) {
// ... other stuff ...
delete x;
}
class Test {
// No longer need "boost:"
friend void checked_delete( Test* x );
};
int main() {
checked_delete( new Test );
}
测试结果表明……(见表 3)。所以,大多数不能处理例1的编译器上的问题就是:显式申明其它命名空间
中的函数模板的一个特化版本为友元(So the problem on most compilers that can't handle Example
1 is specifically declaring friendship for a function template specialization in another
namespace)。(吆,把这句话连念三遍。)唉,发贴者的编译器--Microsoft Visual C++ 6.0,不能
处理这么简单的小问题。
Table 3: The results of compiling Example 3 on various compilers
Compiler Result Error Message
Borland 5.5 OK
Comeau 4.3.0.1 OK
EDG 3.0.1 OK
Intel 6.0.1 OK
gcc 2.95.3 OK
gcc 3.1.1 OK
gcc 3.2 OK
Metrowerks 8.2 OK
MS VC++ 6.0 Error syntax error (just can't handle it)
MS VC++ 7.0 Error friend declaration incorrectly interpreted as declaring a brand-new
(and undefined) ordinary non-template function, even though we used template syntax
MS VC++ 7.1 beta OK
两个行不通的办法
--------------------------------------------------------------------------------
当这个问题出现在USENET上时,一些回应者建议使用using-declaration ( 或,等价的using-directive)
,然后使用没有作用域限定符的友元申明:
namespace boost {
template void checked_delete( T* x ) {
// ... other stuff ...
delete x;
}
}
using boost::checked_delete;
class Test {
~Test() { }
// NOT the template specialization!
friend void checked_delete( Test* x );
};
上述的友元申明将陷入规则#4中: “最后,那个名字必须是无[类或namespace名]限定的,并且申明(或
重复申明)了一普通的 (非模板) 函数。”这实际上是在封闭的命名空间中申明了一个新的普通的非模板
函数,叫作::checked_delete(Test *)。
如果你尝试上述代码,很多编译器会拒绝,提示说checked_delete()未被定义;所有编译器都会拒绝,如
果你试图利用友元关系而在boost::checked_delete()模板中放入私有成员的操作调用。
最后,一个专家建议将它稍微修改一下—使用“using” 且同时使用模板的语法“<>”:
namespace boost {
template void checked_delete( T* x ) {
// ... other stuff ...
delete x;
}
}
using boost::checked_delete;
class Test {
~Test() { }
friend void checked_delete<>( Test* x ); // legal?
};
上面的代码也许不是合法的C++代码-标准没有明确说明它合法,标准委员会中有一个开放的议题要求决
定这是否应该是合法的,倾向是认为它不应该合法;在真实世界中,我所尝试的所有目前版本的编译器都
拒绝它。人们为什么觉得它不应该是合法的?基于一致性(For consistency),因为using存在是为了更
方便地使用名称-以调用函数和在变量和形参申明时使用类型名称。声明则不同:正如你必须在模板所在
的命名空间中申明它的特化版本( 你不能“通过一个using”在另外的命名空间中完成它),因此,你应该
只能够通过模板所在的命名空间的限定来申明一个特化版本为友元( 不能“通过一个using”)(just as
you must declare a template specialization in the template's original namespace (you can't
do it in another namespace "through a using"), so you should only be able to declare a
template specialization as a friend naming the template's original namespace (not "through a
using"))。
总结
--------------------------------------------------------------------------------
为了申明一个函数模板的特化版本为友元,你可以在两个语法中选一个:
// From Example 1
friend void boost::checked_delete ( Test* x );
// From Example 2: add <> or
friend void boost::checked_delete<>( Test* x );
这篇文章已经证明了,如果在例2中不写“<>”或“Test”的话,将要付出高昂的可移植性代价。
指导原则:显式地表明你的意图。当你申明函数模板的一个特化版本为友元时,总是显式加上至少一对“
<>”模板符号。 举例来说是:
namespace boost {
template void checked_delete( T* x );
}
class Test {
friend void boost::checked_delete ( Test* x ); // BAD
friend void boost::checked_delete<>( Test* x ); // GOOD
};
如果你的编译器不支持这两个友元申明中的任何一个,那么,你不得不将必须的函数设为公有-但加一个
注释解释为什么这么做,并提醒只要一升级你的编译器就将它改回私有。[注1]
感谢
--------------------------------------------------------------------------------
Thanks to John Potter for comments on drafts of this material.
注
--------------------------------------------------------------------------------
[1] There are other workarounds, but they're all much more cumbersome. For example, you
could create a proxy class inside namespace boost and befriend that。
▲评论