这篇文章主要是看了王咏刚先生的一篇“关于C++模板和重载的小问题”而发的,可以说是一篇回贴,之所以独立出来是因为这个问题“有趣”:)
先引用一下王咏刚先生的例子:
class Node
{
public:
int m;
Node(int value) : m(value) {}
friend ostream& operator<<(ostream& o, const Node*& p);
};
ostream& operator<<(ostream& o, const Node*& p)
{
o << p->m << endl;
return o;
}
int main()
{
Node* p = new Node(10);
cout << p << endl;
list l;
l.push_back(p);
copy(l.begin(), l.end(), ostream_iterator(cout, "\n"));
}
在王咏刚先生的编译器上,结果是10,0x23455322,也就是说,前者调用了用户重载的哪个operator<<(ostream&,const Node* &)打印出了值,后者则调用了ostream的成员函数operator<<(const void*)打印出了地址。而在我的编译器上两者都打印出地址。
这个问题其实非常简单,可以简化如下(将Node类型替换为int相信大家不会反对吧):
void f(const int* & ); #1
void f(const void* ); #2
int main()
{
int* p=0;
f( p ); //选哪一个?#1? #2?
}
咏刚先生说选#1,因为#1更匹配int*,但事实恰恰相反,#1根本不能匹配!!也就是说,下面的代码从根本上就是错误的:
int* p;
const int* & rp=p; //错误!!!!!
关键在于,对于上面的例子,rp的类型“const int* &”到底是什么?这样才能确定int*能否向它转换。是“int*的const引用”吗?完全错误!应该是“non-const reference to 'const int*' ”!凭什么这样说呢?
这里关键的问题是,到底谁才是const的,即最前面的const到底修饰谁?”,根据C++98标准8.3.2对引用的定义:
对于声明:
T D;
如果D具有如下形式:
& D1 //注意,&与D1之间不能有任何cv修饰符
那么标识符D1的类型为“reference to T”。
这段标准很关键,我们来依葫芦画瓢,对于“const int* & rp;”这个怪胎和罪魁祸首,如果套用上面的格式,则:
T D;
const int* &rp;
这样,D就具有了&D1的形式,其中D1为rp。而T,则是const int*,这是个类型(废话:) ),其含义是“pointer to 'const int'”,因为解析指针的格式与解析引用的格式(上面已经列出)几乎相同,只不过将&换成了*(见C++98 8.3.2)。现在清楚了吗?
“const int* &”的含义是“a non-const reference to T where T is a pointer to 'const int' ”。
之所以用英文来描述是因为在这里变化多端语义微妙的中文实在容易误导。
现在,问题就在于,能否将“int*”转型为“a non-const reference to T where T is a pointer to 'const int' ”呢?你可能会立刻说:“咦?不是能吗?”,并给出转换路径:
int* ——> const int* ——> const int* &
你甚至给出了等价的例子:
int* p=0;
const int* cp=p;
const int* &rcp=cp;
更让你惊喜的是,这段例子竟然通过编译。是的,的确能,而且的确是对的。那问题出在那里呢?我想你忘了最基本的一条规则:不能将non-const的引用绑定到右值(rvalue)。在你给出的转换路径中,从int*转换到const int*创造了一个临时变量,临时变量是右值(rvalue),下面要将这个右值绑定到non-const引用却是万万不能了!至于你给出的例子能通过编译的原因是由于它用一个中间变量cp来承接了转换后的值,cp是个左值(lvalue),可以绑定到non-const引用。至于为什么要有这条古怪的规则是为了避免不可理解的语义,考虑下面的例子:
int i= 0;
double& rd=i; //错,左值不能绑定到non-const引用
rd=10;
这里,i转换为一个double临时变量,这个变量无法绑定到double&。所以是错误的。试想,如果允许这个例子通过编译,那么程序员会认为rd绑定到了i,从而“rd=10;”改变了i的值,事实恰恰相反,rd只不过绑定到了一个临时的double,改变的是个临时变量,i没有变。这就导致了语义模糊。而如果这里绑定到的是个const引用就不同了——“rd=10;”根本不能通过编译,也就杜绝了错误。
对于“const int* &”这个古怪的类型,一个常见的错误就是将int*放在一起而将const孤立出来分析,从而导致错误的理解为:“a const reference to 'int*' ”。C++标准和我们开了个不大不小的玩笑,这主要是由于加入了引用造成的,或者,干脆从语法上说,是加入了“&”符号造成的,另一个原因是继承了C里面的劣根性——“const int i”和“int const i”表示同一个意思“一个整型常量”。
[蛇足],其实“const引用”这个称谓不妥当,就连C++标准里都说“const reference”,其实如果要求和指针的说法一致,应该为“reference to const T”。之所以连C++标准都这样称呼可能是由于reference 其值本来就是const的,所以偷了个懒。而且reference一旦绑定导某个对象,说该reference其实就是再说那个对象,所以"const reference"就不难理解为“const的'那个对象'”了。而对于指针就不同了,“const int* p和int * const p”是完全不同的,前者是“pointer to const int”,后者是“const pointer to int”,大不相同。C++标准偷懒没有错,但是这种“const reference”的说法却是会误导人的。一般人喜欢将“&”符号读作“reference”,“const reference”就意味着const在前reference在后,而“const int* &”恰恰符合这个概念,所以很容易在一念之间就犯了错。C++标准的制定者也许没有想到一个简便称呼会带来这个隐晦的篓子吧。反之,如果强调“reference to const”,则程序员拿到这个类型就会先考虑“reference to what?”这个问题,从而得到正确的答案。所以,建议大家在阅读这种类型的时候,最好反过来读:先将“* cv-qualifier p”或“& ref”剥离开来,然后看剩下的是什么类型,这样就一清二楚了。另外,平时编码时勤用typedef,例如,上面的例子如果用typedef就不会出篓子了:
typedef int* int_ptr;
void f(const int_ptr& ); #1
void f(const void* ); #2
int main()
{
int_ptr p=0;
f( p ); //调用#1
}
因为int_ptr从语义上来说是个整体,是“一个”类型,所以按照你通常的理解方式,就不会出错了。但是还要注意,千万不能将typedef换成宏,宏的替换发生在所有的编译期动作(词法分析,语法分析,语义分析等)之前,是纯粹的文本替换。而typedef是有语义内涵的。用宏只会换汤不换药。
BTW.
咏刚先生用的编译期可能允许将临时变量绑定到non-const引用,这就是为什么在他的例子里面,第一次输出 “cout<<p”会成功。也就是为什么在他的编译器下,Node* 可以转型为const Node* &的原因。
至于咏刚先生所说的“模板参数推导改变了参数类型”则是没有的,原因如下:
Node * const & 其实可以看作是 Node* const,因为引用就代表着被引用的对象本身,至于为什么不能绑定到 const Node* &,我猜是由于其转换路径所致:
Node* const ——> const Node* ——> const Node* &
这里编译器的逻辑可能是“第一步转型把指针值本身的const修饰给去掉了,不行”。可惜事实并非如此,因为第一步转换只是产生一个临时变量,属于值拷贝,这种const丢失并没有错,例如:
const int & i=0;
int j=i; //ok
本文地址:http://com.8s8s.com/it/it28121.htm