大雨。
烏云象鉛塊一樣低低的壓了下來(lái),豆大的雨滴打的玻璃窗啪啪作響,難得一見(jiàn)的異常天氣正在竭力表現(xiàn)它令人討厭的一面。不過(guò)這一切似
乎并沒(méi)有影響到
Solmyr,他仍然以他習(xí)慣的舒適姿勢(shì)半躺在寬大的椅子里,手里還托著一杯熱騰騰的果汁,在他背后,zero
在鍵盤(pán)上敲打著什么。
“唉,Solmyr
,標(biāo)準(zhǔn)庫(kù)中的
stack
怎么會(huì)是這個(gè)樣子?設(shè)計(jì)糟透了。”zero
停止了工作,轉(zhuǎn)過(guò)身來(lái)面對(duì)
Solmyr
,看起來(lái)有些困惑。
“胡亂批評(píng)被納入神圣標(biāo)準(zhǔn)的成員是會(huì)遭天遣的。”Solmyr
低著頭,以一種算命先生似的語(yǔ)調(diào)答道。
不知道上天是否打算加強(qiáng)
Solmyr
的說(shuō)服力,恰在此時(shí)天空劃過(guò)一道閃電,藍(lán)白色的電光掙扎著努力向地面撲來(lái),緊接著就是“喀喇”一聲巨響
——— 這個(gè)雷很近。
一秒鐘前還在想“這未免也太扯了”的
zero
表情一下子變得很古怪,良久才恢復(fù)正常。他標(biāo)出了兩行代碼接著說(shuō)到:“好、好吧,Solmyr,那請(qǐng)你解釋一下為什么
stack
的界面是這個(gè)樣子。”
std::stack<int>
si;
……
int i = si.top();
si.pop();
“只要讓
pop()
返回棧頂元素就可以把上面兩行合成一行,而且更加直觀,為什么要搞成現(xiàn)在這樣?”
目睹了
zero
表情變化的
Solmyr
強(qiáng)忍住放聲大笑的沖動(dòng)
——— 老天知道他忍的有多辛苦 ———
緩緩的把杯子放到桌上,轉(zhuǎn)過(guò)身來(lái)開(kāi)始講解這個(gè)問(wèn)題:
“原因在于異常。”
“異常?”
“對(duì),
很多代碼在沒(méi)有異常的時(shí)候工作的挺好,但是一旦出現(xiàn)異常就變得不可收拾,就像一間茅草屋,平時(shí)看起來(lái)沒(méi)什么問(wèn)題,一遇到今天這種天氣
…… ”,Solmyr
指了指窗外,“
…… 立刻就會(huì)垮掉??紤]一下如果 pop()
返回棧頂元素需要怎樣實(shí)現(xiàn),假設(shè)棧內(nèi)部用數(shù)組實(shí)現(xiàn),且不考慮棧是否為空的問(wèn)題。”
“很簡(jiǎn)單啊。”,zero
打開(kāi)了編輯器,寫(xiě)下:
template
<typename T>
T stack<T>::pop()
{
...
...
return data[top--]; // 假設(shè)數(shù)據(jù)存儲(chǔ)于數(shù)組
data
中,top
代表?xiàng)m斘恢?
}
Solmyr 搖搖頭:“這就是茅草屋。要知道
stack
是個(gè)模板類(lèi),它存放的元素
T
可能是用戶(hù)定義的類(lèi)。我來(lái)問(wèn)你,如果類(lèi)型
T
的拷貝構(gòu)造函數(shù)拋出異常,會(huì)出現(xiàn)什么情況?”
“嗯
…… 按值返回,返回值是個(gè)臨時(shí)對(duì)象,該臨時(shí)對(duì)象以
data[top]
拷貝構(gòu)造
…… 嗯,這樣一來(lái)函數(shù)返回時(shí)可能拋出異常,客戶(hù)此時(shí)無(wú)法取得該元素。”
“還有呢?”
“還有?”
“提示,你的
top
怎么了?”
“
…… 哎呀!糟了!top
此時(shí)已經(jīng)減一,棧頂元素就此丟失了!這樣的話(huà)
…… 必須實(shí)現(xiàn)一個(gè)函數(shù)允許客戶(hù)修改 ……”,zero
說(shuō)不下去了。他想了一會(huì),搖搖頭承認(rèn)失?。?#8220;不行,這里拷貝構(gòu)造發(fā)生在函數(shù)返回之后,無(wú)論如何無(wú)法避免這種情況。只能在文檔里寫(xiě)明:要求
T
的拷貝構(gòu)造函數(shù)不拋出異常。”
zero
停了一停,小心翼翼的問(wèn)
Solmyr
:“這個(gè)不算過(guò)分的要求吧?”
Solmyr
的回答異常簡(jiǎn)短:“new”
“哦對(duì),new
在分配內(nèi)存失敗時(shí)會(huì)拋出
std::bad_alloc
…… 算我沒(méi)說(shuō)。Solmyr
,我明白了,為了處理異常的情況,調(diào)整棧頂位置必須在所有數(shù)據(jù)拷貝完成之后,所以按值返回是不可接受的。”
“正確。所以對(duì)于一個(gè)設(shè)計(jì)目標(biāo)是最大限度可復(fù)用性的標(biāo)準(zhǔn)庫(kù)成員而言,這是不可接受的。”
Solmyr
頓了頓,繼續(xù)說(shuō)到:“而且異常帶來(lái)的影響遠(yuǎn)不止此。我剛才說(shuō)‘假設(shè)棧內(nèi)部用數(shù)組實(shí)現(xiàn)’,但如果你充分考慮拋出異常的各種可能性,你就會(huì)發(fā)現(xiàn)用數(shù)組實(shí)現(xiàn)是糟糕的主意。”
“
…… …… …… …… ……
這是為什么?在沒(méi)有傳值返回的情況下,我們總可以捕捉到發(fā)生的異常并加以處理啊?”,zero
謹(jǐn)慎的發(fā)問(wèn)。
Solmyr
贊許的看著
zero
。“發(fā)問(wèn)之前先自行思考,習(xí)慣不錯(cuò)。”,Solmyr
心想,但是臉上一點(diǎn)也沒(méi)表現(xiàn)出來(lái):“沒(méi)錯(cuò),但捕捉到異常不代表你總能正確的處理它。考慮一下
stack
的賦值運(yùn)算符,如果我們用數(shù)組來(lái)實(shí)現(xiàn),那么在拷貝數(shù)據(jù)的時(shí)候肯定會(huì)有類(lèi)似這樣的一個(gè)循環(huán):”
//
各變量的意義與上面相同
template
<typename T>
stack<T>& stack<T>::perator=(const
stack<T>& rhs)
{
... ...
for(int i=0; i<rhs.top; i++)
data
= rhs.data;
...
...
}
“現(xiàn)在考慮類(lèi)型
T
的賦值運(yùn)算符可能拋出異常,該怎樣修改上面的代碼。”
Solmyr
停了下來(lái),再度捧起了杯子。
“用
try
把
…… 哦 …… …… …… …… …… ……”,zero
似乎發(fā)現(xiàn)了問(wèn)題所在,沉默良久,才接著說(shuō)到:“這個(gè)循環(huán)可能在運(yùn)行到一半的時(shí)候拋出異常,這樣會(huì)導(dǎo)致一部分?jǐn)?shù)據(jù)已經(jīng)成功賦值,另一部分卻還是老的。除非我
們用 catch(...)
捕捉所有異常,忽略之并繼續(xù)賦值。”
“但是這樣
……”,Solmyr
有意識(shí)的引導(dǎo)
zero
繼續(xù)深入思考。
“……
但是這樣,賦值運(yùn)算符拋出的異常就被我們‘吃掉了’,異??偸谴碇承┎辉摪l(fā)生的事情發(fā)生了,所以應(yīng)該讓客戶(hù)接收到這個(gè)異常才對(duì)。”
zero
皺著眉頭,一字一頓,顯得相當(dāng)辛苦。
“正
確。stack
作為一個(gè)通用的標(biāo)準(zhǔn)庫(kù)成員,在面對(duì)異常時(shí)必須做到兩點(diǎn)。一、異常安全,也就是說(shuō)異常不會(huì)導(dǎo)致它本身處于一種錯(cuò)誤的狀態(tài)或是導(dǎo)致數(shù)據(jù)丟失或是造成資源泄漏;
二、異常透明,也就是說(shuō)客戶(hù)代碼 ——— 這里指它存放的類(lèi)型
T
的實(shí)現(xiàn)
——— 拋出的任何異常,不應(yīng)該被‘吃掉’或者被改變,應(yīng)該透明的傳遞給客戶(hù)。一望即知,上面的代碼無(wú)可能同時(shí)做到這兩點(diǎn)。”
“是這樣,我懂了,這大概就是標(biāo)準(zhǔn)庫(kù)中的
stack
不用數(shù)組實(shí)現(xiàn)的主要原因了吧”,zero
露出了很有把握的神情。
“當(dāng)然不是!有點(diǎn)常識(shí)好不好,用數(shù)組實(shí)現(xiàn)的話(huà)
stack
的大小固定,這怎么能夠接受呢?!”
又一次的,Solmyr
目睹了
zero
表情發(fā)生難以言喻的劇烈變化。這次他沒(méi)能忍住放聲大笑的沖動(dòng),連杯子里的果汁也灑了出來(lái),一時(shí)間,笑聲充滿(mǎn)了整個(gè)辦公室
——— 不僅僅是他的,還包括了(眾位看官應(yīng)該猜的到吧?)圍觀同事們的笑聲。
驅(qū)散了圍觀者之后,zero
面帶慍色的坐下:“有那么好笑嗎?”
“抱
歉抱歉,我 …… 哈哈哈 …… 我 …… 哈哈 ……
我只是一時(shí)忍不住 …… 哈哈哈哈 …… ”,Solmyr
好容易平息了大笑,坐直了身子,放下了果汁,正色道:“關(guān)鍵在于上面引入的應(yīng)該遵循的兩條原則,也就是異常安全,和異常透明。現(xiàn)在你考慮一下如果
stack
內(nèi)部的數(shù)據(jù)以指針存放,怎樣在賦值運(yùn)算符中保證上述兩點(diǎn)?”
“
…… 嗯
…… 還是會(huì)有上面那樣一個(gè)循環(huán) …… 呃 …… ”,zero
面有難色。
“提示,不一定非得直接拷貝到
stack
保存數(shù)據(jù)的內(nèi)存里。”
“
…… 嗯
…… 不直接拷貝,那么就是 …… 就是拷貝到 ……
啊!我明白了!”,zero
抓住了其中的關(guān)鍵,飛快的寫(xiě)下:
//
pdata 代表指向存放數(shù)據(jù)內(nèi)存的指針,top
代表?xiàng)m斣氐钠屏?
template
<typename T>
stack<T>& stack<T>::perator=(const
stack<T>& rhs)
{
... ...
T*
ptemp = new T[rhs.top];
try
{
for(int i=0; i<rhs.top;
i++)
*(ptemp+i)
= *(rhs.pdata+i);
}
catch(...)
// 捕捉可能出現(xiàn)的異常
{
delete[] ptemp;
throw; // 重新拋出
}
delete[] pdata; // 釋放當(dāng)前的內(nèi)存
pdata
= ptemp; // 讓
pdata
指向賦值成功的內(nèi)存塊
...
...
}
“只
要這樣”,zero
邊輸入邊說(shuō),“只要先把數(shù)據(jù)拷貝到一個(gè)臨時(shí)分配的緩沖區(qū),在此過(guò)程中處理異常,然后讓
pdata
指向成功分配的內(nèi)存就行了。這里的關(guān)鍵是讓拷貝動(dòng)作成為可以
…… 呃 …… 可以安全的取消的,剩下的賦值動(dòng)作就是簡(jiǎn)單的指針賦值,肯定不會(huì)拋出異常了。”
“非常好。值得指出的是,這是一種相當(dāng)常見(jiàn)的手段,有個(gè)名字叫做
copy
& swap ,它不僅僅可以用來(lái)應(yīng)付異常,也可以有效的實(shí)現(xiàn)一些其他特征。OK,這個(gè)問(wèn)題大概就是這樣了。”
問(wèn)題似乎可以告一段落了,Solmyr
開(kāi)始打算就此結(jié)束這個(gè)話(huà)題。可
zero
疑惑的表情阻止了他。
“還有什么問(wèn)題嗎?zero
?”
“啊
……
沒(méi)什么,我只是在想,異常導(dǎo)致了這么多麻煩,這一次,還有上一次的線程死鎖問(wèn)題(參見(jiàn)“小品文系列”的前一篇,“成對(duì)出現(xiàn)”)都是因?yàn)楫惓5拇嬖诓艜?huì)變得如此復(fù)雜的,那為什么
C++
還要支持它呢?有錯(cuò)誤完全可以在返回值里報(bào)告嘛。”
“嗯,
這確實(shí)是個(gè)常見(jiàn)的疑惑,不過(guò)答案也很簡(jiǎn)單,異常的存在有它自己的價(jià)值。一、使用異常報(bào)告錯(cuò)誤可以避免污染函數(shù)界面;二、如果你希望報(bào)告比較豐富的錯(cuò)誤信
息,使用一個(gè)異常對(duì)象比簡(jiǎn)單的返回值要有效的多,而且避免了返回復(fù)雜對(duì)象造成的開(kāi)銷(xiāo);三、也是我認(rèn)為比較重要的,有些錯(cuò)誤不合適用返回值來(lái)報(bào)告。舉個(gè)例
子,動(dòng)態(tài)內(nèi)存分配。我問(wèn)你,C
語(yǔ)言中怎樣報(bào)告動(dòng)態(tài)內(nèi)存分配錯(cuò)誤?”,Solmyr
轉(zhuǎn)過(guò)頭來(lái)看著
zero
。
“malloc
函數(shù)返回一個(gè)
NULL
值代表動(dòng)態(tài)內(nèi)存分配錯(cuò)誤。”
“但是你見(jiàn)過(guò)多少
C
程序員在每次使用
malloc
之后都檢查返回值?”
“
…… ”
“沒(méi)有是嗎?這很正常,每次使用
malloc
之后檢查返回值是件令人痛苦的事情,所以即使有
Steve
Maguire(注:《Writing
Clean Code》一書(shū)的作者)這樣的老程序員諄諄教導(dǎo)、耳提面命,還是有數(shù)以萬(wàn)計(jì)的
C
程序中存在這樣的代碼:”,Solmyr
順手鍵入:
/*
傳統(tǒng)
C
程序
*/
int* p = malloc( sizeof(int) );
*p = 10;
“一旦
malloc
失敗返回
NULL,這個(gè)程序就會(huì)崩潰。然而如果是
C++
程序,使用
new
的話(huà)
…… ”,Solmyr
鍵入了對(duì)應(yīng)的代碼:
//
C++ 程序
int*
p = new int;
*p = 10;
“就不存在這樣的問(wèn)題。我問(wèn)你,這是為什么?”
zero
很快找到了答案:“因?yàn)槿绻?
new
失敗,它會(huì)拋出
std::bad_alloc
異常,于是函數(shù)在此中斷、退出,下面這一行也就不會(huì)被調(diào)用了。”
“正
確。而且你不必在每一處處理這個(gè)異常,你只要保證你的程序?qū)Ξ惓M该?,就可以?
main
函數(shù)中寫(xiě)下
try
... catch 對(duì),捕獲所有未捕獲的異常。比如你可以在
main
函數(shù)中捕捉
std::bad_alloc,在輸出‘內(nèi)存不足’錯(cuò)誤信息,然后保存所有未保存的數(shù)據(jù),完成所有的清理工作,最后結(jié)束程序。一言以蔽之,體面的退出。”
zero
點(diǎn)著頭,喃喃的重復(fù)著:“對(duì),體面的退出。”
見(jiàn)
zero
領(lǐng)會(huì)了他的意思,Solmyr
繼續(xù)開(kāi)始下一個(gè)議題:“異常的存在還有最后一個(gè)重要價(jià)值
——— 也是當(dāng)初設(shè)計(jì)它的初衷之一 ———
提供一個(gè)通用的手段讓構(gòu)造函數(shù)可以方便的報(bào)告錯(cuò)誤:因?yàn)闃?gòu)造函數(shù)沒(méi)有返回值。”
“還有析構(gòu)函數(shù)也是。”沒(méi)等
Solmyr
說(shuō)完,zero
就加上了這一句。
Solmyr
對(duì)著自作聰明的
zero
搖了搖頭:“不要想當(dāng)然,關(guān)于異常有一個(gè)非常重要的原則:永遠(yuǎn)不要讓你的析構(gòu)函數(shù)拋出異常。知道為什么嗎?”
“
…… 不知道。”
zero
這次決定老實(shí)承認(rèn)。
“因?yàn)閽伋霎惓5奈鰳?gòu)函數(shù)會(huì)導(dǎo)致最簡(jiǎn)單的程序無(wú)法正確運(yùn)行,比如下面兩句:”這次出現(xiàn)在屏幕上的,是看來(lái)似乎毫無(wú)瑕疵的兩行代碼:
evil
p = new evil[10];
delete[] p;
“看上去一點(diǎn)問(wèn)題也沒(méi)有是么?仔細(xì)分析一下
delete[]
p 這一句,它會(huì)調(diào)用
10
次
evil
類(lèi)的析構(gòu)函數(shù),假設(shè)其中第
5
次
evil
類(lèi)的析構(gòu)函數(shù)拋出異常,會(huì)出現(xiàn)什么情況?”
zero
陷入了沉思,視線盯著屏幕一動(dòng)不動(dòng),神情看起來(lái)就象是一段執(zhí)行復(fù)雜運(yùn)算的程序,而且是沒(méi)有輸出的那種。不過(guò)沒(méi)多久,zero
就換了一種表情,這種表情通常被形容為胸有成竹:“我知道了
Solmyr
,在這種情況下,delete[]
面臨兩難選擇。選擇一是不捕捉這個(gè)異常,讓它傳播到調(diào)用者那里,但這樣一來(lái)
delete[]
就被中斷了,后面的
5
個(gè)
evil
對(duì)象占用的內(nèi)存就會(huì)無(wú)法釋放,導(dǎo)致資源泄漏;選擇二是捕捉這個(gè)異常以防止資源泄漏,但這樣一來(lái)這個(gè)異常就被
delete[]
吃掉了,違反了‘對(duì)異常透明’的原則。所以無(wú)論怎么做,都沒(méi)法妥善的處理析構(gòu)函數(shù)拋出異常的情況。”
Solmyr
贊許的點(diǎn)頭:“非常好。接下來(lái),你的任務(wù)是
……”
“我知道我知道,把這些討論整理成文檔是吧?我這就動(dòng)手。”
zero
轉(zhuǎn)過(guò)身去,開(kāi)始埋頭于他的文檔。而
Solmyr
則再度恢復(fù)了半躺半坐的舒適姿勢(shì),捧起了他的果汁,并且略略有些意外的發(fā)現(xiàn)
———
天氣放晴了。