關於C++模板和重載的小問題

關於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);
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章