关于C++模板和重载的小问题

类别:编程语言 点击:0 评论:0 推荐:

关于C++模板和重载的小问题

前几天和一位朋友讨论了有关C++模板和重载的一个小问题。我记得最初发现问题的代码是这样的:

#include <iostream>
#include <list>
using namespace std;

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<Node*> l;
 l.push_back(p);
 copy(l.begin(), l.end(), ostream_iterator<Node*>(cout, "\n"));
}

上面的代码先用“cout << p”这样的方式显示Node内容,因为前面已经重载了“<<”运算符,显示结果为10,正确。但接下来用copy函数直接把list中的元素“复制”到cout中,以显示Node内容时,程序只显示出了p中存储的内存地址,而非p所指向的Node的内容,这与预期的结果不符合。

我以前没用过ostream_iterator加copy这种直接复制到输出流中的语法(这可以省去自己写循环的麻烦),不是很熟悉其中的机制。一开始,我认为这是因为STL中的copy函数仅是简单地做赋值操作,没有调用重载的operator<<函数,所以没有显示出Node的内容。但很快,那位朋友就指出,虽然copy函数中做的是赋值操作,但ostream_iterator类重载了赋值运算符:

ostream_iterator<_Ty, _Elem, _Traits>& operator=(const _Ty& _Val)
{ // insert value into output stream, followed by delimiter
 *_Myostr << _Val;
 if (_Mydelim != 0)
  *_Myostr << _Mydelim;
 return (*this);
}

这段重载过的代码会调用“<<”运算符函数。也就是说,copy函数没有得到预期结果,这里面一定还有其他我们没有注意到的问题。我仔细想了想,觉得这个问题可以用C++关于模板、重载以及类型隐式转换顺序的语法解释清楚。

首先,对程序的跟踪表明,ostream_iterator的operator=在执行“<<”运算符时,调用的是basic_ostream类中重载的“<<”运算符函数:

_Myt& operator<<(const void *_Val);

这说明,C++编译器在选择参数类型以确定调用哪个重载函数时,选择了const void*,而非我们自己定义的const Node*&。这时我注意到,定义我们自己的operator<<函数时,参数p既然已经是const Node*了,就没必要再加&修饰符了,最简洁的定义方式应该是:

friend ostream& operator<<(ostream& o, const Node* p);

果然,把前面的代码改成纯指针的定义后,copy函数也正确地显示了数字10,这是我们期望的结果,说明copy函数这回正确调用了我们重载的“<<”运算符。可为什么简单的增加一个“&”会让C++编译器调用另一个重载函数呢?

我做了个简单的实验:

void foo(const int*& p)
{
 cout << "A" << endl;
}

void foo(const int* p)
{
 cout << "B" << endl;
}

int main()
{
 int i = 10;
 int* p = &i;
 foo(p);
}

这段代码的运行结果是A,这说明,当实参类型是指针时,C++编译器会优先匹配那个带&修饰符(即参数为引用类型)的重载函数。这不是和上面的情况正好相反吗?传给copy函数的list里存储的是Node*,basic_ostream类中重载的“<<”参数类型为const void*,而我们原先重载的参数类型为const Node*&,为什么这一回编译器就不调用我们重载的函数呢?这是不是说明Node*经过copy函数内的几次转换后,类型已经不是Node*了呢?

果然,跟踪一下程序的运行过程就会发现,当copy调用ostream_iterator重载的operator=函数时,实参类型是Node*,而operator=函数的形式参数类型是const _Ty&:

ostream_iterator<_Ty, _Elem, _Traits>& operator=(const _Ty& _Val)

这里,_Ty是模板参数,实际就是我们在copy函数里注明的Node*,而组合到const _Ty&中,参数的类型就变成了:

Node* const &

上面这个变化可以从VC.NET的调试器中看到。这说明ostream_iterator重载的operator=函数已经把实参的类型改成了另一种样子(已经不是单纯的指针了),接下来调用“<<”时,编译器就会选择const void*这样的匹配,而非const Node*&。

是不是越说越乱了。还是从头把思路整理一遍吧:

第一、对于下面这样的模板函数:

template<class T> void foo(const T val);

当T表示的类型是指针如int*时,const和T的结合体是int* const,而非字面上看到的const int*,这可以用下面的代码来证明:

template<class T> void foo(const T val) {}

int main()
{
 int i;
 
 const int* p = &i;
 foo<int*>(p); // 编译出错
 
 int* const q = &i;
 foo<int*>(q); // 可以正确编译运行
}

在C++中,int* const和const int*是完全不同的两种类型,前者const是修饰指针,后者const是修饰指针所指向的值。

第二、对于这样的一组重载函数:

void foo(const int* p);
void foo(int* const p);

当我们用int* const型的指针作为实参调用时,编译器会选择第2个函数,因为第2个函数的参数类型和实参类型完全相同。但对于这样一组重载函数:

void foo(const int* p);
void foo(const int*& p);

当我们同样用int* const型的指针作为实参调用时,编译器会选择第1个函数,因为两个函数参数类型和实参类型都不同,编译器会调用最接近的那个类型(参数的隐式转换匹配顺序,可以参考C++标准中的相关说明)。

这实际就是我们上面遇到的那个问题的答案。basic_ostream类中重载的“<<”参数类型为const void*,我们原先重载的参数类型为const Node*&,而ostream_iterator重载的operator=函数在调用“<<”运算符时,实参类型已经被改成了Node* const &,因此,编译器调用的是ostream_iterator重载的函数,而非我们重载的函数。

所以,当我们把最上面那段程序的“<<”修改为

friend ostream& operator<<(ostream& o, const Node* p);

时,程序可以给出正确的结果。但根据上面的讨论,如果我们把该函数的定义改成:

friend ostream& operator<<(ostream& o, Node* const & p);

程序也可以给出正确的结果。

这只是个小问题,而且是那位朋友编程时偶然遇到的,不过这个问题说明,在C++语言里,参数的定义、隐式转换以及匹配顺序相当重要也相当复杂(C++标准文档里关于这个东西的说明有好长一段),我就很容易在这些地方犯糊涂。

另外,上面的实验都是在VS.NET的C++编译器中做的。那位朋友也在VC6下做了相同的实验,但结果却完全不同。例如,最上面那段程序在VC6下,无论参数类型是指针还是引用,都无法得到正确的结果;奇怪的是,在VC6下,当参数类型是指针时,如果把Node类和“<<”函数的的定义都统统放进std namespace中,居然就可以得到正确结果了。这似乎说明,VC6中的C++编译器在参数匹配顺序方面,并不完全符合C++ 1998标准的定义(也许VC6会优先匹配同一个namespace中的重载函数)。

 

版权声明:CSDN是本Blog托管服务提供商。如本文牵涉版权问题,CSDN不承担相关责任,请版权拥有者直接与文章作者联系解决。

posted on 2004年08月25日 5:33 PM

Feedback
# 回复:关于C++模板和重载的小问题 2004-08-26 12:32 AM 刘未鹏
咏刚先生想必没有查过标准吧。在我的VC7.1上编译你的例子,两者都输出地址,而这样才是正确的。
关于这个问题我写了一篇回帖在
http://blog.csdn.net/pongba
上,名为“who is const?!”,下面是关键的回答:

这个问题的形式其实非常简单,可以简化如下(将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;
更让你惊喜的是,这段例子竟然通过编译。是的,的确能,而且的确是对的。但我还是要强调,int*不能转换为“const int* &”!那问题出在那里呢?我想你忘了最基本的一条规则:不能将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”表示同一个意思“一个整型常量”。

 

# 回复:关于C++模板和重载的小问题 2004-08-26 12:34 AM 刘未鹏
至于其它编译器我没有试过,但是问题肯定只在两个方面,一个是对“const int* &”这个类型的语法分析上出错,另一个是错误的允许了rvalue绑定到non-const引用

# 回复:关于C++模板和重载的小问题 2004-08-26 12:29 PM 王咏刚
今天刚上网,就看到刘未鹏兄的回复,谢谢!

我昨天用的编译器版本是:

Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80x86
Copyright (C) Microsoft Corporation 1984-2002. All rights reserved.

谢谢你的提醒,我对标准文档的理解与你相同,但对这个问题的认识还和你不大一样。所以,正在重新测试和思考中……


# 回复:关于C++模板和重载的小问题 2004-08-26 1:50 PM 刘未鹏
我用的VC Express 2005,这个版本的编译期的major version增了一,是:14.00.40607.16。另外在gcc3.3上面也通过了,用的是dev.c++.4.90


# 回复:关于C++模板和重载的小问题 2004-08-26 3:42 PM 王咏刚

我明白了,昨天我被Microsoft在编译器中的扩展属性欺骗了,参见下面这个链接里的相关内容:

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vccore/html/_core_microsoft_extensions_to_c.asp
Microsoft Extensions to C and C++
Passing a Non-Const Pointer Parameter to a Function that Expects a Reference to a Const Pointer Parameter

也就是说,像下面这样的语义是VC对C++标准的扩展:

int* p = 0;
const int* & r = p;

那么,我在《关于C++模板和重载的小问题》一文中讲的参数匹配的顺序实际上都是在VC这个扩展的前提下才成立了(我用的是VS.NET 2003中的13.10.3077版)。难怪相关的代码在gcc或其他编译器上得不出相同的结果。在VC中,如果用编译选项把扩展语法关掉,上面这种转换就不成立了。Borland C++ 5.5倒是可以给出与VC大致相同的结果,这说明Borland与Microsoft的协同程度的确不错。

回过头再比较一下,在gcc中(我用的是Dev-C++ 4.9.9.0),如果编译

int* p = 0;
const int* & r = (const int*)p;

这样的代码,就可以正确编译通过,其产生的汇编指令是:

mov DWORD PTR [ebp-4],0x0
mov eax,DWORD PTR [ebp-4]
mov DWORD PTR [ebp-8],eax
lea eax,[ebp-8]
mov DWORD PTR [ebp-12],eax

这说明,gcc在将int*强制转换为const int*时的确生成了中间存储区,并将中间存储区的指针赋予引用变量,以避免非const的引用变量无法接受右值的问题。当然,在这样的处理之后,我们对r的后续操作就不会再对p的取值产生影响了。

而在VC中(我用的是VS.NET 2003中的13.10.3077版),如果打开扩展选项,可以正确编译下面的代码:

int* p = 0;
const int* & r = p;

其产生的汇编指令是:

mov dword ptr [p],0
lea eax,[p]
mov dword ptr [r],eax

这说明,VC在编译时没有使用中间存储区,而是简单地将原指针的地址赋予引用变量。这种取巧的做法也同样避免了非const的引用变量无法接受右值的问题。--是不是就因为有这样简单和取巧的途径,VC才会引入这个扩展的语法呢?

-------------------------------------------

感谢刘未鹏兄的提醒,我昨天写了那些文字后,差一点就自以为是下去了。
另:刘未鹏兄对const int* &这种声明真正含义的讲解方法非常直观易懂,对于typedef的用法也说得相当到位,。

 

# 回复:关于C++模板和重载的小问题 2004-08-26 5:16 PM 王咏刚

关于模板参数推导的问题,其实,const加上模板参数的类型组合方式,也可以按照理解typedef的方式来认识:模板参数本身表示一个类型的整体,前面加的const是修饰整体而非局部。比如:

template<class T>void f(const T val);

当T为int*时,T的语义是“整数的指针”,而const这时修饰的是“指针”而非“整数”,所以,经过这样的限定,最终的val类型是“整数的常量指针(int* const,指针本身不可变)”,而非“常量整数的指针(const int*,指针所指的值不可变)”

所以,当我们把int*传入上面的模板函数时,参数类型被改变(其实是限定,C++标准里叫cv-qualified)成了int* const。这也就是ostream_iterator& operator=(const _Tp& __value)这个函数接受指针参数时所做的事情(const和后面的引用修饰符无关)。

在下面的示例代码里,两次对重载函数的调用都会输出“B”:

#include <iostream>
using namespace std;

void foo(const int* p)
{
cout << "A" << endl;
}
void foo(int* const p)
{
cout << "B" << endl;
}

typedef int* int_ptr;
void goo(const int* p)
{
cout << "A" << endl;
}
void goo(const int_ptr p)
{
cout << "B" << endl;
}

template<class T>void f(const T val)
{
foo(val);
goo(val);
}

int main()
{
int* p = 0;
f(p);
}

本文地址:http://com.8s8s.com/it/it23297.htm