国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費電子書等14項超值服

開通VIP
C 與C++中的異常處理

Robert Schmidt 著
無情 譯
目 錄
1. 異常和標(biāo)準(zhǔn)C 對它的支持...............................................................................................2
2. Microsoft 對異常處理方法的擴展..............................................................................12
3. 標(biāo)準(zhǔn)C++異常處理的基本語法和語義..........................................................................27
4. 實例剖析EH....................................................................................................................33
5. C++的new 和delete 操作時的異常處理.....................................................................40
6. Microsoft 對于<new>的實現(xiàn)版本中的異常處理........................................................47
7. 部分構(gòu)造及placement delete....................................................................................53
8. 自動刪除,類屬new 和delete、placement new 和placement delete ...............59
9. placement new 和placement delete,及處理構(gòu)造函數(shù)拋出的異常....................68
10. 從私有子對象中產(chǎn)生的異常.........................................................................................74
11. 異常規(guī)格申明.................................................................................................................83
12. unexpected()的實現(xiàn)上固有的限制.............................................................................89
13. 異常安全.........................................................................................................................94
14. 模板安全.......................................................................................................................100
15. 模板安全(續(xù))...........................................................................................................107
16. 指導(dǎo)方針.......................................................................................................................113
17. C++異常和Visual C++ SEH 的混合使用...................................................................120
1. 異常和標(biāo)準(zhǔn)C 對它的支持
(前言略)
1.1 異常分類
基于Dr. GUI 的建議,我把我的第一個專欄投入到“程序異?!钡南盗猩?。我認(rèn)識到,
“exception”這個術(shù)語有些不明確并和上下文相關(guān),尤其是C++標(biāo)準(zhǔn)異常(C++ standard
exceptions)和Microsoft 的結(jié)構(gòu)化異常(structured exception handling)。不幸的的是,
“異常”一詞太常見了,隨時出現(xiàn)在語言的標(biāo)準(zhǔn)和常見的編程文獻中。因為不想創(chuàng)造一個新
名詞,所以我將盡力在此系列的各部分中明確我對“異常”的用法。
?? Part 1 概述通常意義上的異常的性質(zhì),和標(biāo)準(zhǔn)C 庫提供的處理它們的方法。
?? Part 2 縱覽Microsoft 對這些標(biāo)準(zhǔn)C 庫方法的擴展:專門的宏和結(jié)構(gòu)化異常處理。
?? Part 3 及其余將致力于標(biāo)準(zhǔn)C++異常處理體系。
(C 語言使用者可能在Part2 后放棄,但我鼓勵你堅持到底;我所提出的許多點子同樣
適用于C,雖然不是很直接。)
本質(zhì)上看,程序異常是指出現(xiàn)了一些很少發(fā)生的或出乎意料的狀態(tài),通常顯示了一個程
序錯誤或要求一個必須提供的回應(yīng)。不能滿足這個回應(yīng)經(jīng)常造成程序功能削弱或死亡,有時
導(dǎo)致整個系統(tǒng)和它一起down 掉。不幸的是,試圖使用傳統(tǒng)的防護方法來編制健壯的代碼經(jīng)
常只是將一個問題(意外崩潰)換成了另外一個問題(更混亂的設(shè)計和代碼)。
太多的程序員認(rèn)為這個交換抵不上程序意外崩潰時造成的煩惱,于是選擇了生活在危險
之中。認(rèn)識到這一點后,C++標(biāo)準(zhǔn)增加了一個優(yōu)雅并且基本上不可見的“異常體系”到語言
中;就這樣,這個方法產(chǎn)生了。如同我們在Part4 的開始部分將要看到的,這個方法大部分
情況下很成功,但在很微妙的情況下可能失敗。
1.2 異常的生命階段
在這個系列里,我將展示C 和C++處理異常體系運行于異常整個生命期的每一階段時的
不同之處:
?? 階段1:一個軟件錯誤發(fā)生。這個錯誤也許產(chǎn)生于一個被底層驅(qū)動或內(nèi)核映射為軟件錯
誤的硬件響應(yīng)事件(如被0 除)。
?? 階段2:錯誤的原因和性質(zhì)被一個異常對象攜帶。這個對象的類型可以簡單的整數(shù)值到
繁雜的C++類對象。
?? 階段3:你的程序必須檢測這個異常對象:或者輪詢它的存在,或者由其主動上報。
?? 階段4:檢測代碼必須決定如何處理異常。典型的方法分成三類。
a 忽略異常對象,并期望別人處理它。
b 在這個對象上干些什么,并還允許別人再繼續(xù)處理它。
c 獲得異常的全部所有權(quán)。
?? 階段5:既然異常已經(jīng)處理了,程序通常恢復(fù)并繼續(xù)執(zhí)行。恢復(fù)分成兩種:
a 恢復(fù)異常,從異常發(fā)生處繼續(xù)執(zhí)行。
b 終止異常,從異常被處理處繼續(xù)執(zhí)行。
當(dāng)在程序外面(由運行期庫或操作系統(tǒng))終止異常時,恢復(fù)經(jīng)常是不可能的,程序?qū)?br>常結(jié)束。
我故意忽略了硬件錯誤事件,因為它們完全是底層平臺范圍內(nèi)的事。取而代之,我假定
一些軟件上的可檢測錯誤已經(jīng)發(fā)生,并產(chǎn)生了一個處于第一階段的軟件異常對象。
1.3 C 標(biāo)準(zhǔn)庫異常處理體系
C 標(biāo)準(zhǔn)庫提供了幾個方法來處理異常。它們也全部在標(biāo)準(zhǔn)C++中有效,只是相關(guān)的頭文
件名字變了:老的C 標(biāo)準(zhǔn)頭文件<name.h>映射到了新的C++標(biāo)準(zhǔn)頭文件<cname>。(頭文件名
的前綴“C”是個助記符,暗示著這些全是C 庫頭文件。)
雖然基于向后兼容性,老的C 頭文件也被C++保留,但我建議你盡可能使用新的頭文件。
對于絕大部分實際使用而言,最大的變化是在新的頭文件中,申明的函數(shù)被包含在命名空間
std 內(nèi)。舉個例子,C 語言使用
#include <stdio.h>
FILE *f = fopen("blarney.txt", "r");
在C++中被改成
#include <cstdio>
std::FILE *f = std::fopen("blarney.txt", "r");
或更C 風(fēng)格的
#include <cstdio>
using namespace std;
FILE *f = fopen("blarney.txt", "r");
不幸的是,Microsoft 的Visual C++沒有將這些新的頭文件包含在命名空間std 中,雖
然這是C++標(biāo)準(zhǔn)所要求的(subclause D.5)。除非Visual C++在這些頭文件中已經(jīng)正確地支
持了std,我將一直在我的專欄中使用老式的C 風(fēng)格命名。
(象MIcrosoft 這樣的運行庫賣主這么做是合理的,正確地實現(xiàn)這些C 程序庫的頭文件
極可能要求維護和測試兩份完全不同的底層代碼,這是不可能受歡迎的也不值得多花力氣的
工作。)
1.4 無條件終止
僅次于徹底忽略一個異常,大概最容易的異常處理方法是程序自我毀滅。有時,最懶的
方法事實上是最正確的。
在你開始嘲笑以前,應(yīng)該認(rèn)識到,一些異常表示的狀況是如此嚴(yán)重以致于怎么也不可能
合理恢復(fù)的。也許最好的例子就是malloc 時返回NULL。如果空閑堆管理程序不能提供可用
的連續(xù)空間,你程序的健壯性將嚴(yán)重受損,并且恢復(fù)的可能性是渺茫的。
C 庫頭文件<stdlib.h>提供了兩個終止程序的函數(shù):abort()和exit()。這兩個函數(shù)運
行于異常生命期的4 和5。它們都不會返回到其調(diào)用者中,并都導(dǎo)致程序結(jié)束。這樣,它們
就是結(jié)束異常處理的最后一步。
雖然兩個函數(shù)在概念上是相聯(lián)系的,但它們的效果不同:
?? abort():程序異常結(jié)束。默認(rèn)情況下,調(diào)用abort()導(dǎo)致運行期診斷和程序自毀。它
可能會也可能不會刷新緩沖區(qū)、關(guān)閉被打開的文件及刪除臨時文件,這依賴于你的編譯
器的具體實現(xiàn)。
?? exit():文明地結(jié)束程序。除了關(guān)閉文件和給運行環(huán)境返回一個狀態(tài)碼外,exit()還調(diào)
用了你掛接的atexit()處理程序。
一般調(diào)用abort()處理災(zāi)難性的程序故障。因為abort()的默認(rèn)行為是立即終止程序,
你就必須負(fù)責(zé)在調(diào)用abort()前存儲重要數(shù)據(jù)。(當(dāng)我們談?wù)摰?lt;signal.h>時,你可以使得
abort()自動調(diào)用clean up 代碼。)
相反,exit()執(zhí)行了掛接在atexit()上的自定義clean up 代碼。這些代碼被按照其掛
接的反序執(zhí)行,你可以把它們當(dāng)作虛擬析構(gòu)器。通過必要的clean up 代碼,你可以安全地
終止程序而沒有留下尾巴。例如:
#include <stdio.h>
#include <stdlib.h>
static void atexit_handler_1(void)
{
printf("within 'atexit_handler_1' ");
}
static void atexit_handler_2(void)
{
printf("within 'atexit_handler_2' ");
}
int main(void)
{
atexit(atexit_handler_1);
atexit(atexit_handler_2);
exit(EXIT_SUCCESS);
printf("this line should never appear ");
return 0;
}
/* When run yields
within 'atexit_handler_2'
within 'atexit_handler_1'
and returns a success code to calling environment.
*/
(注意,即使是程序從main()正常返回而沒有明確調(diào)用exit(),所掛接的atexit()代
碼仍然會被調(diào)用。)
無論abort()還是exit()都不會返回到它的調(diào)用者中,且都將導(dǎo)致程序結(jié)束。在這個意
義上來說,它們都表現(xiàn)為終止異常的最后一步。
1.5 有條件地終止
abort()和exit()讓你無條件終止程序。你還可以有條件地終止程序。其實現(xiàn)體系是每
個程序員所喜愛的診斷工具:斷言,定義于<assert.h>。這個宏的典型實現(xiàn)如下所示:
#if defined NDEBUG
#define assert(condition) ((void) 0)
#else
#define assert(condition)
_assert((condition), #condition, __FILE__, __LINE__)
#endif
如定義體所示,當(dāng)宏NDEBUG 被定義時斷言是無行為的,這暗示了它只對調(diào)試版本有效。
于是,斷言條件從不在非調(diào)試版本中被求值,這會造成同樣的代碼在調(diào)試和非調(diào)試版本間有
奇妙的差異。
/* debug version */
#undef NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d ", i);
return 0;
}
/* When run yields
i is 1
*/
現(xiàn)在,通過定義NDEBUG,從debug 版變到release 版:
/* release version */
#defing NDEBUG
#include <assert.h>
#include <stdio.h>
int main(void)
{
int i = 0;
assert(++i != 0);
printf("i is %d ", i);
return 0;
}
/* When run yields
i is 0
*/
要避免這個差異,必須確保斷言表達(dá)式的求值不會包含有影響的副作用。
在僅供調(diào)試版使用的定義體中,斷言變成呼叫_assert()函數(shù)。我起了這個名字,而你
所用的運行庫的實現(xiàn)可以調(diào)用任何它想調(diào)用的內(nèi)部函數(shù)。無論它叫什么,這個函數(shù)通常有以
下形式:
void _assert(int test, char const *test_image,
char const *file, int line)
{
if (!test)
{
printf("Assertion failed: %s, file %s, line %d ",
test_image, file, line);
abort();
}
}
所以,失敗的斷言在調(diào)用abort()前顯示出失敗情況的診斷條件、出錯的源文件名稱和
行號。我在這里演示的診斷機構(gòu)“printf()”相當(dāng)粗糙,你所用的運行庫的實現(xiàn)可能產(chǎn)生更
多的反饋信息。
斷言處理了異常的階段3 到5。它們實際上是一個帶說明信息的abort()并做了前提條
件檢查,如果檢查失敗,程序中止。一般使用斷言調(diào)試邏輯錯誤和絕不可能出現(xiàn)在正確的程
序中的情況。
/* 'f' never called by other programs */
static void f(int *p)
{
assert(p != NULL);
/* ... */
}
對比一下邏輯錯誤和可以存在于正確程序中的運行期錯誤:
/* ...get file 'name' from user... */
FILE *file = fopen(name, mode);
assert(file != NULL); /* questionable use */
這樣的錯誤表示異常情況,但不是bug。對這些運行期異常,斷言大概不是個合適的處
理方法,你應(yīng)該用我下面將介紹的另一個體系來代替。
1.6 非局部的跳轉(zhuǎn)
與刺激的abort()和exit()相比,goto 語句看起來是處理異常的更可行方案。不幸的是,
goto 是本地的:它只能跳到所在函數(shù)內(nèi)部的標(biāo)號上,而不能將控制權(quán)轉(zhuǎn)移到所在程序的任
意地點(當(dāng)然,除非你的所有代碼都在main 體中)。
為了解決這個限制,C 函數(shù)庫提供了setjmp()和longjmp()函數(shù),它們分別承擔(dān)非局部
標(biāo)號和goto 作用。頭文件<setjmp.h>申明了這些函數(shù)及同時所需的jmp_buf 數(shù)據(jù)類型。
原理非常簡單:
?? setjmp(j)設(shè)置“jump”點,用正確的程序上下文填充jmp_buf 對象j。這個上下文包
括程序存放位置、棧和框架指針,其它重要的寄存器和內(nèi)存數(shù)據(jù)。當(dāng)初始化完jump 的
上下文,setjmp()返回0 值。
?? 以后調(diào)用longjmp(j,r)的效果就是一個非局部的goto 或“長跳轉(zhuǎn)”到由j 描述的上下
文處(也就是到那原來設(shè)置j 的setjmp()處)。當(dāng)作為長跳轉(zhuǎn)的目標(biāo)而被調(diào)用時,setjmp()
返回r 或1(如果r 設(shè)為0 的話)。(記住,setjmp()不能在這種情況時返回0。)
通過有兩類返回值,setjmp()讓你知道它正在被怎么使用。當(dāng)設(shè)置j 時,setjmp()如你
期望地執(zhí)行;但當(dāng)作為長跳轉(zhuǎn)的目標(biāo)時,setjmp()就從外面“喚醒”它的上下文。你可以用
longjmp()來終止異常,用setjmp()標(biāo)記相應(yīng)的異常處理程序。
#include <setjmp.h>
#include <stdio.h>
jmp_buf j;
void raise_exception(void)
{
printf("exception raised ");
longjmp(j, 1); /* jump to exception handler */
printf("this line should never appear ");
}
int main(void)
{
if (setjmp(j) == 0)
{
printf("'setjmp' is initializing 'j' ");
raise_exception();
printf("this line should never appear ");
}
else
{
printf("'setjmp' was just jumped into ");
/* this code is the exception handler */
}
return 0;
}
/* When run yields:
'setjmp' is initializing 'j'
exception raised
'setjmp' was just jumped into
*/
那個填充jmp_buf 的函數(shù)不在調(diào)用longjmp()之前返回。否則,存儲在jmp_buf 中的上
下文就有問題了:
jmp_buf j;
void f(void)
{
setjmp(j);
}
int main(void)
{
f();
longjmp(j, 1); /* logic error */
return 0;
}
所以,你必須把setjmp()處理成只是到其所在位置的一個非局部跳轉(zhuǎn)。
Longjmp()和setjmp()聯(lián)合體運行于異常生命期的2 和3 階段。longjmp(j,r)產(chǎn)生異常
對象r(一個整數(shù)),并且作為返回值傳送到setjmp(j)處。實際上,setjmp()函數(shù)通報了異
常r。
1.7 信號
C 函數(shù)庫也提供了標(biāo)準(zhǔn)的(雖然原始的)“事件”處理包。這個包定義了一組事件和信
號,以及標(biāo)準(zhǔn)的方法來觸發(fā)和處理它們。這些信號或者表示了一個異常狀態(tài)或者表示了一個
不協(xié)調(diào)的外部事件;基于所談?wù)摰闹黝},我將只集中討論異常信號。
為了使用這些包,需要包含標(biāo)準(zhǔn)頭文件<signal.h>。這個頭文件申明了函數(shù)raise()和
signal(),數(shù)據(jù)類型sig_atomic_t,和以SIG 開頭的信號事件宏。標(biāo)準(zhǔn)要求有六個信號宏,
也許你所用的運行庫實的現(xiàn)會再附加一些。這些信號被固定死在<signal.h>中,你不能增加
自定義的信號。信號通過調(diào)用raise()產(chǎn)生并被處理函數(shù)捕獲。運行時體系提供默認(rèn)處理函
數(shù),但你能通過signal()函數(shù)安裝自己的處理函數(shù)。處理函數(shù)可以通過sig_atomic_t 類型
的對象和外部進行通訊;如類型名所示,對這樣的對象的操作是原子操作或者說中斷安全的。
當(dāng)你掛接信號處理函數(shù)時,通常提供一個函數(shù)地址,這個的函數(shù)必須接受一個整型值(所
要處理的信號事件),并且無返回。這樣,信號處理函數(shù)有些象setjmp();它們所收到的僅
有的異常信息是單個整數(shù):
void handler(int signal_value);
void f(void)
{
signal(SIGFPE, handler); /* register handler */
/* ... */
raise(SIGFPE); /* invoke handler, passing it 'SIGFPE' */
}
只可其一地,你可以安裝兩個特別的處理函數(shù):
?? signal(SIGxxx,SIG_DFL),為指定的信號掛接系統(tǒng)的缺省處理函數(shù)。
?? signal(SIGxxx,SIG_IGN),告訴系統(tǒng)忽略指定的信號。
signal()函數(shù)返回前次掛接的處理函數(shù)的地址(表明掛接成功),或返回SIG_ERR(表
明掛接失?。?br>處理函數(shù)被調(diào)用表明信號正在試圖恢復(fù)異常。當(dāng)然,你可以在處理函數(shù)中隨意地調(diào)用
abort()、exit()或longjmp(),有效地將信號解釋為終止異常。有趣的是,abort()自己事
實上在內(nèi)部調(diào)用了raise(SIGABRT)。SIGABRT 的缺省處理函數(shù)發(fā)起了一個診斷并終止程序,
當(dāng)然你可以安裝自己的處理函數(shù)來改變這個行為。不能改變的是abort()的終止程序的行
為。Abort()理論上的實現(xiàn)如下:
void abort(void)
{
raise(SIGABRT);
exit(EXIT_FAILURE);
}
也就是說,即使你的SIGABRT 處理函數(shù)返回了,abort()仍然中止你的程序。
C 語言標(biāo)準(zhǔn)在信號處理函數(shù)的行為上增加了一些限制和解釋。如果你有C 語言標(biāo)準(zhǔn),我
建議你查閱條款7.7.1.1 的細(xì)節(jié)。(很不幸,C 語言和C++語言的標(biāo)準(zhǔn)在Internet 都得不到。)
<signal.h>的申明覆蓋了異常的整個生存期,從產(chǎn)生到死亡。在標(biāo)準(zhǔn)的C 語言運行期庫
中,它們是最接近于異常完全解決方案的。
1.8 全局變量
<setjmp.h>和<signal.h>一般使用異常通知體系:當(dāng)試圖通知一個異常事件時喚醒一個
處理函數(shù)。如果你更愿意使用輪詢體系,C 標(biāo)準(zhǔn)庫在<errno.h>提供了例子。這個頭文件定
義了errno 及其一些可能的取值。標(biāo)準(zhǔn)要求這樣三個值:EDOM、ERANGE 和EILSEQ,分別適
用于域、范圍和多字節(jié)順序錯誤,你的編譯器可能又加了些其它的,它們?nèi)宰帜浮癊”開
頭。
errno,通過由運行庫的代碼設(shè)置它而用戶代碼查詢它的辦法將二者聯(lián)系起來,運行于
異常生命期的1 到3:運行庫產(chǎn)生異常對象(一個簡單的整數(shù)),把值拷給errno,然后依賴
用戶的代碼去輪詢和檢測這個異常。
運行庫主要在<math.h>和<stdio.h>的函數(shù)中使用errno。errno 在程序開始時設(shè)為0,
函數(shù)庫程序不會再次把它設(shè)為0。因此,要檢測錯誤,你必須先將errno 設(shè)為0,再調(diào)用運
行庫程序,調(diào)用完后檢查errno 的值:
#include <errno.h>
#include <math.h>
#include <stdio.h>
int main(void)
{
double x, y, result;
/* ... somehow set 'x' and 'y' ... */
errno = 0;
result = pow(x, y);
if (errno == EDOM)
printf("domain error on x/y pair ");
else if (errno == ERANGE)
printf("range error on result ");
else
printf("x to the y = %d ", (int) result);
return 0;
}
注意:errno 不一定要綁在一個對象上:
int *_errno_function()
{
static int real_errno = 0;
return &real_errno;
}
#define errno (*_errno_function())
int main(void)
{
errno = 0;
/* ... */
if (errno == EDOM)
/* ... */
}
你可以在自己的程序中采用這樣的技巧,對errno 及其值進行模擬。使用C++的話,你
當(dāng)然可以把這種策略擴展到類或命名空間的對象和函數(shù)上。(實際上,在C++中,這個技巧
是Singleton Pattern 的基礎(chǔ)。)
1.9 返回值和回傳參數(shù)
象errno 這樣的異常對象不是沒有限制的:
?? 所有相關(guān)聯(lián)的部分必須一致,確保設(shè)置和檢查同一個對象。
?? 無關(guān)的部分可能意外地修改了對象。
?? 如果沒有在調(diào)用程序前重設(shè)對象,或在調(diào)用下一步前沒有檢查它們,你就可能漏了異常。
?? 宏和內(nèi)部代碼中的對象在重名時將掩蓋異常對象。
?? 靜態(tài)對象天生就不是(多)線程安全的。
總之,這些對象很脆弱:你太容易用錯它們,編譯器沒有警告程序卻有不可預(yù)測的行為。
要排除這些不足,你需要這樣的對象:
?? 被兩個正確的部分訪問--一個產(chǎn)生異常,一個檢測異常。
?? 帶有一個正確的值。
?? 名字不能被掩蓋
1.10 線程安全。
函數(shù)返回值滿足這些要求,因為它們是無名的臨時變量,由函數(shù)產(chǎn)生而只能被調(diào)用者訪
問。調(diào)用一完成,調(diào)用者就可以檢查或拷貝返回值;然后原始的返回對象將消失而不能被重
用。又因為是無名的,它不能被掩蓋。
(對于C++,我假設(shè)只有右值函數(shù)調(diào)用表達(dá),也就是說不能返回引用。由于我限定現(xiàn)在
只談?wù)揅 兼容的技巧,而C 不支持引用,這樣的假設(shè)是合理的。)
返回值出現(xiàn)在異常生命期的階段2。在調(diào)用和被調(diào)用函數(shù)的聯(lián)合體中,這只是完整的異
常處理的一部分:
int f()
{
int error;
/* ... */
if (error) /* Stage 1: error occurred */
return -1; /* Stage 2: generate exception object */
/* ... */
}
int main(void)
{
if (f() != 0) /* Stage 3: detect exception */
{
/* Stage 4: handle exception */
}
/* Stage 5: recover */
}
返回值是C 標(biāo)準(zhǔn)庫所喜歡的異常傳播方法。看下面的例子:
if ((p = malloc(n)) == NULL)
/* ... */
if ((c = getchar()) == EOF)
/* ... */
if ((ticks = clock()) < 0)
/* ... */
注意,典型的C 習(xí)慣用法:在同一條語句中接收返回值和檢測異常。這種壓縮表達(dá)式重
載一個通道(返回值對象)來攜帶兩個不同的含義:合法的數(shù)據(jù)值和異常值。代碼必須按兩
條路來解釋這個通道,直到知道哪個是正確的。
這種函數(shù)返回值的用法常見于很多語言中,尤其是Microsoft 開發(fā)的語言無關(guān)的
Component Object Model (COM)。COM 方法通過返回一類型為HRESULT(特別安排的32 位無
符號值)的對象提示異常。和剛討論的例子不同,COM 的返回值只攜帶狀態(tài)和異常信息;回
傳信息通過參數(shù)列表中的指針進行。
回傳指針和C++的引用型的參數(shù)是函數(shù)返回值的變形,但有些明顯的不同:
?? 你能忽略和丟棄返回值?;貍鲄?shù)則綁定到了相應(yīng)的實參上,所以不可能完全忽略它們。
和返回值相比,參數(shù)在函數(shù)和它們的調(diào)用者間形成了緊耦合。
?? 通過回傳參數(shù)可以返回任意個數(shù)的值,而通過返回值只能返回一個值。所以回傳參數(shù)提
供了多個返回值。
?? 返回值是臨時對象:它們在調(diào)用前不存在,并且在調(diào)用結(jié)束是消失。實參的生命期遠(yuǎn)長
于函數(shù)的調(diào)用過程。
1.11 小結(jié)
這次大概地介紹了異常和標(biāo)準(zhǔn)C 對它的傳統(tǒng)支持。第二部分,我將研究Microsoft 對標(biāo)
準(zhǔn)C 方法的擴展:特有的異常處理宏、結(jié)構(gòu)化異常處理或說SEH。我將總結(jié)所有C 兼容方法
(包括SEH)的局限性,并在第三部分拉開C++異常的序幕。
回到目錄
2. Microsoft 對異常處理方法的擴展
前次,我概述了異常的分類和C 標(biāo)準(zhǔn)庫支持的處理方法。這次討論Microsoft 對這些方
法的擴展:結(jié)構(gòu)化異常處理(SEH)和Microsoft Foundation Class (MFC)異常處理。SEH
對C 和C++都有效,MFC 異常體系只對C++有效。
2.1 機構(gòu)化異常處理
機構(gòu)化異常處理是Windows 提供的服務(wù)功能并對所有語言寫的程序有效。在Visual C++
中,Microsoft 封裝和簡化了這些服務(wù)(通過非標(biāo)準(zhǔn)的關(guān)鍵字和庫程序)。Windows 平臺的其
它編譯器可能選擇不同的方式來到達(dá)相似的結(jié)果。在這個專欄中,名詞“Structured
Exception Handling”和“SEH”專指Visual C++對Windows 異常服務(wù)的封裝。
2.2 關(guān)鍵字
為了支持SEH,Micorsoft 用四個新關(guān)鍵字?jǐn)U展了C 和C++語言:
?? __except
?? __finally
?? __leave
?? __try
因為這是非標(biāo)關(guān)鍵字,必須打開擴展選項后再編譯(關(guān)掉/Fa)。
為什么這些關(guān)鍵字帶下劃線?C++標(biāo)準(zhǔn)(條款17.4.3.1.2,“Global names”)規(guī)定:
下列名字和函數(shù)總是保留給編譯器:
?? 所有帶雙下劃線(__)或以一個下劃線加一個大寫字母開始的名字保留給編譯器隨意使
用。
?? 所有以一個下劃線開始的名字保留給編譯器作全局名稱用。
C 標(biāo)準(zhǔn)有類似的申明。
既然SEH 的關(guān)鍵字符合上面的規(guī)則,Microsoft 就有權(quán)這樣使用它們。這也表明,你不
被允許在自己的程序中使用保留的名字。你必須避免定義名字類似__MYHEADER_H__或
_FatalError 的標(biāo)識符。
有趣而又不幸地,Visual C++的application wizards 產(chǎn)生的源代碼使用了保留的標(biāo)識
符。例如,如果你用ATL COM App Wizard 生成一個新的service,結(jié)果框架代碼定義了如
_Handler 和_twinMain 的名字--標(biāo)準(zhǔn)所說的你的程序不能使用的保留名稱。
要減少這個不合規(guī)定行為,你當(dāng)然可以手工更改這些名稱。還好,這些有疑問的名字都
是類的私有變量,在類的定義外面是不可見的,在.h 和.cpp 中進行全局替換是可行的。不
幸的是,有一個函數(shù)(_twinMain)和一個對象(_Module)被申明了extern,也就是說程
序的其它部分會假定你使用了這些名字。(事實上,Visual C++庫libc.lib 在連接時需要名
字_twinMain 可用。)
我建議你保留Wizard 生成的名字,不要在你自己的代碼中定義這樣的名字就可以了。
另外,你應(yīng)該將所有不合標(biāo)準(zhǔn)的定義寫入文檔并留給程序的維護人員;記住,Visual C++
以后的版本(和現(xiàn)有的其它C++編譯器)可能以另外的方式使用這些名字,從而破壞了你的
代碼。
2.3 標(biāo)識符
Microsoft 也在非標(biāo)頭文件excpt.h 中定義了幾個SEH 的標(biāo)識符,并且包含入windows.h
中。在其內(nèi)部,定義了:
?? 供__except 的過濾表達(dá)式使用的過濾結(jié)果宏。
?? Win32 對象和函數(shù)的別名宏,用于查詢異常信息和狀態(tài)。
?? 偽關(guān)鍵字宏,和前面談到的四個關(guān)鍵字有著相同名字和含義,但沒有下劃線。(例如,
宏leave 對應(yīng)SEH 關(guān)鍵字__leave。)
Microsoft 用這些宏令我抓狂。他們對同一個函數(shù)了定義多個別名。例如,excpt.h 有
如下申明和定義:
unsigned long __cdecl _exception_code(void);
#define GetExceptionCode _exception_code
#define exception_code _exception_code
也就是說,你可以用三種方法調(diào)用同一函數(shù)。你用哪個?并且,這些別名會如你所期望
地被維護嗎?
在Microsoft 的文檔中,它看起來偏愛GetExceptionCode,它的名字和其它全局Windows
API 函數(shù)風(fēng)格一致。我在MSDN 中搜索到33 處GetExceptionCode,兩個_exception_code,
而exception_code 個數(shù)為0。根據(jù)Microsoft 的引導(dǎo),推薦使用GetExceptionCode 及類似
名稱的其它函數(shù)。
因為_exception_code 的兩個別名是宏,所以你不能再使用同樣的名字了。我曾經(jīng)犯過
這個錯,當(dāng)我在為這個專欄寫例程的時候。我定義了一個局部對象叫exception_code(大
概是吧)。實際上我就是定義了一個局部對象叫_exception_code,這是我無意中使用的宏
exception_code 展開的結(jié)果。當(dāng)我一想到是這個問題,解決方案就是簡單地將我的對象名
字從exception_code 改為code。
最后,excpt.h 定義了一個特別的宏--“try”--已經(jīng)成為C++真正的關(guān)鍵字的東西。
這意味著你不能在包含了excpt.h 的編譯單元中簡單地混合SEH 和標(biāo)準(zhǔn)C++的異常塊,除非
你愿意#undef 這個try 宏。當(dāng)這樣undef 而露出真正的try 關(guān)鍵字時,要冒搞亂SEH 的維
護人員大腦的危險。另一方面,精通標(biāo)準(zhǔn)C++的程序員會將try 理解為一個關(guān)鍵字而不是宏。
我認(rèn)為,包含一個頭文件(即使是象excpt.h 這樣的非標(biāo)頭文件)不應(yīng)該改變符合語言
標(biāo)準(zhǔn)的代碼的行為。我更堅持掩蓋或重定義掉語言標(biāo)準(zhǔn)定義的關(guān)鍵字是個壞習(xí)慣。我建議:
#undef try,同樣不使用其它的偽關(guān)鍵字宏,直接使用真正的關(guān)鍵字(如__try)。
2.4 語法
最基本的SEH 語法是try 塊。如下形式:
__try compound-statement handler
處理體:
__except ( filter-expression ) compound-statement
或:
__finally compound-statement
完整一點看,try 塊如下:
__try
{
...
}
__except(filter-expression)
{
...
}
或:
__try
{
...
}
__finally
{
...
}
在__try 里面你必須使用一個leave 語句:
__try
{
...
__leave;
...
}
在更大的程序塊中,一個try 塊被認(rèn)為是個單條語句:
if (x)
{
__try
{
...
}
__finally
{
...
}
}
等價于:
if (x)
__try
{
...
}
__finally
{
...
}
其它注意點:
?? 在給定的try 塊中你必須有一個正確的異常處理函數(shù)。
?? 所有的語句必須合并。即使只有一條語句跟在__try、__except 或__finally 后面也必
須將它放入{}中。
?? 在異常處理函數(shù)中,相應(yīng)的過濾表達(dá)式必須有一個或能轉(zhuǎn)換為一個int 型的值。
2.5 基本語意
上次我列舉了異常生命期的5 個階段。在SEH 體系下,這些階段實現(xiàn)如下:
?? 操作系統(tǒng)上報了一個硬件錯誤或檢測到了一個軟件錯誤,或用戶代碼檢測到一個錯誤
(階段1)。
?? (通常是由用戶調(diào)用Win32 函數(shù)RasieException 啟動,)操作系統(tǒng)產(chǎn)生并觸發(fā)一個異常
對象(階段2)。這個對象是一個結(jié)構(gòu),其屬性對異常處理函數(shù)可見。
?? 異常處理函數(shù)“看到”異常,并且有機會捕獲它(階段3 和4)。取決于處理函數(shù)的意
愿,異常將或者恢復(fù)或者終止。(階段5)。
一個簡單的例子:
int filter(void)
{
/* Stage 4 */
}
int main(void)
{
__try
{
if (some_error) /* Stage 1 */
RaiseException(...); /* Stage 2 */
/* Stage 5 of resuming exception */
}
__except(filter()) /* Stage 3 */
{
/* Stage 5 of terminating exception */
}
return 0;
}
Microsoft 調(diào)用定義在__except 中的異常處理函數(shù),和定義在__finally 中的終止函數(shù)。
一旦異常被觸發(fā),由__except 開始的異常處理函數(shù)被異常發(fā)生點順函數(shù)調(diào)用鏈向外面
詢問。每個被發(fā)現(xiàn)的異常處理函數(shù),其過濾表達(dá)式都被求值。每次求值后發(fā)生什么取決于其
返回結(jié)果。
excpt.h 定義了3 個過濾結(jié)果的宏,都是int 型的:
?? EXCEPTION_CONTINUE_EXECUTION = -1
?? EXCEPTION_CONTINUE_SEARCH = 0
?? EXCEPTION_EXECUTE_HANDLER = 1
前面我說過,過濾表達(dá)式必須兼容int 型,所以它們和這3 個宏的值匹配。這個說法太
保守了:我的經(jīng)驗顯示Visual C++接受的過濾表達(dá)式可以具有所有的整型、指針型、結(jié)構(gòu)、
數(shù)組甚至是void 型!(但我在嘗試浮點指針時遇到了編譯錯誤。)
更進一步,所有求出的值看來都有效(至少對整型如此)。所有非零且符號位為0 的值
效果相當(dāng)于EXCEPTION_EXECUTE_HANDLER , 而符號位為1 的相當(dāng)于
EXCEPTION_CONTINUE_EXECUTION。這大概是按位取模的結(jié)果。
如果一個異常處理函數(shù)的過濾求值結(jié)果是EXCEPTION_CONTINUE_SEARCH,這個處理函數(shù)
拒絕捕獲異常,將繼續(xù)搜索下一個異常處理函數(shù)。
通過由過濾表達(dá)式產(chǎn)生一個非EXCEPTION_CONTINUE_SEARCH 來捕獲異常,一旦捕獲,程
序就恢復(fù)。怎么恢復(fù)仍然由過濾表達(dá)式的值決定:
?? EXCEPTION_CONTINUE_EXECUTION:表現(xiàn)為恢復(fù)異常。從發(fā)生異常處下面開始執(zhí)行。異常
處理函數(shù)本身的代碼不執(zhí)行。
?? EXCEPTION_EXECUTE_HANDLER:表現(xiàn)為終止異常。從異常發(fā)生處開始退棧,一路上所遇
到終止函數(shù)都被執(zhí)行。棧退到捕獲異常的處理函數(shù)所在的一級為止。進入處理函數(shù)體并
執(zhí)行。
如名所示,終止處理函數(shù)(以__finally 開始的代碼)在終止異常時被調(diào)用。里面是clean
up 代碼,它們就象C 標(biāo)準(zhǔn)庫中的atexit()函數(shù)和C++的析構(gòu)函數(shù)。終止處理函數(shù)在正常執(zhí)
行流程也會進入,就象不是捕獲型代碼。相反,異常處理函數(shù)總表現(xiàn)為捕獲型:它們只在其
過濾表達(dá)式求值為EXCEPTION_EXECUTE_HANDLER 時才進入。
終止處理函數(shù)并不明確知道自己是從正常流程進入的還是在一個try 塊異常終止時進
入的。要判斷這點,可以調(diào)用AbnormalTermination 函數(shù)。此函數(shù)返回一個int,0 表明是
從正常流程進入的,其它值表明在異常終止時進入的。
AbnormalTermination 實際上是個指向_abnormal_termination()的宏。Visual C++將
_abnormal_termination()設(shè)計為環(huán)境敏感的函數(shù),就象一個關(guān)鍵字。你不能隨便調(diào)用這個
函數(shù),只能在終止處理函數(shù)中調(diào)用。這意味著你不能在終止處理函數(shù)中調(diào)用一個中間函數(shù),
再在此中間函數(shù)中調(diào)用_abnormal_termination(),這樣做會得到一個編譯期錯誤。
2.6 例程
下面的C 例子顯示了不同的過濾表達(dá)式值和處理函數(shù)本身類型的相互作用。第一個版本
是個小的完整程序,以后的版本都在它前面一個上有小小的改動。所有的版本都自解釋的,
你能看清流程和行為。
程序通過RaiseException()觸發(fā)一個異常對象。RaiseException()函數(shù)的第一個參數(shù)
是異常的代碼,類型是32 位無符號整型(DWORD);Microsoft 為用戶自定義的錯誤保留了
[0xE0000000,0xEFFFFFFF]的范圍。其它參數(shù)一般填0。
這里使用的異常過濾器很簡單。實際使用中,大概要調(diào)用GetExceptionCode()和
GetExceptionInformation()來查詢異常對象的屬性。
2.7 Version #1: Terminating Exception
用Visual C++生成一個空的Win32 控制臺程序,命名為SEH_test,選項為默認(rèn)。將下
列C 源碼加入工程文件:
#include <stdio.h>
#include "windows.h"
#define filter(level, status)
(
printf("%s:%*sfilter => %s ",
#level, (int) (2 * (level)), "", #status),
(status)
)
#define termination_trace(level)
printf("%s:%*shandling %snormal termination ",
#level, (int) (2 * (level)), "",
AbnormalTermination() ? "ab" : "")
static void trace(int level, char const *message)
{
printf("%d:%*s%s ", level, 2 * level, "", message);
}
extern int main(void)
{
DWORD const code = 0xE0000001;
trace(0, "before first try");
__try
{
trace(1, "try");
__try
{
trace(2, "try");
__try
{
trace(3, "try");
__try
{
trace(4, "try");
trace(4, "raising exception");
RaiseException(code, 0, 0, 0);
trace(4, "after exception");
}
__finally
{
termination_trace(4);
}
end_4:
trace(3, "continuation");
}
__except(filter(3, EXCEPTION_CONTINUE_SEARCH))
{
trace(3, "handling exception");
}
trace(2, "continuation");
}
__finally
{
termination_trace(2);
}
trace(1, "continuation");
}
__except(filter(1, EXCEPTION_EXECUTE_HANDLER))
{
trace(1, "handling exception");
}
trace(0, "continuation");
return 0;
}
現(xiàn)在編譯代碼。(可能會得到label end_4 未用的警告;先忽略。)
注意:
?? 程序有四個嵌套try 塊,兩個有異常處理函數(shù),兩個有終止處理函數(shù)。為了更好地顯示
嵌套和控制流程,我把它們?nèi)糠湃胪粋€函數(shù)中。實際編程中可能是放在多個函數(shù)或
多個編譯單元中的。
?? 追蹤運行情況,輸出結(jié)果顯示當(dāng)前塊的嵌套層次。
?? 異常過濾器被實現(xiàn)為宏。第一個參數(shù)是嵌套層次,第二個才是實際要處理的值。
?? 終止處理函數(shù)通過termination_trace 宏跟蹤其執(zhí)行情況,顯示出調(diào)用它們的原因。
(記住,終止處理函數(shù)即使沒有發(fā)生異常也會進入的。)
運行此程序,將看到如下輸出:
0:before first try
1: try
2: try
3: try
4: try
4: raising exception
3: filter => EXCEPTION_CONTINUE_SEARCH
1: filter => EXCEPTION_EXECUTE_HANDLER
4: handling abnormal termination2: handling abnormal termination
1: handling exception
0:continuation
事件鏈:
?? 第四層try 塊觸發(fā)了一個異常。這導(dǎo)致順嵌套鏈向上搜索,查找愿意捕獲這個異常的異
常過濾器。
?? 碰到的第一個異常過濾器(在第三層)得出了EXCEPTION_CONTINUE_SEARCH,所以拒絕
捕獲這個異常。繼續(xù)搜索下一個異常處理函數(shù)。
?? 碰到的下一個異常過濾器(在第一層)得出了EXCEPTION_EXECUTE_HANDLER。這次,這
個過濾器捕獲這個異常。因為它求得的值,異常將被終止。
?? 控制權(quán)回到異常發(fā)生點,開始退棧。沿路所有的終止處理函數(shù)被運行,并且所有的處理
函數(shù)都知道異常終止發(fā)生了。一直退棧到控制權(quán)回到捕獲異常的異常處理函數(shù)(在第一
層)。在退棧時,只有終止處理函數(shù)被執(zhí)行,中間的其它代碼被忽略。
?? 控制權(quán)一回到捕獲異常的異常處理函數(shù)(在第一層),將以正常狀態(tài)繼續(xù)執(zhí)行。
注意,控制權(quán)在同一嵌套層傳遞了兩次:第一次異常過濾表達(dá)式求值,第二次在退棧和
執(zhí)行終止處理函數(shù)時。這造成了一種危害可能:如果一個異常過濾表達(dá)式以某種終止處理函
數(shù)不期望的方式修改了的什么。一個基本原則就是,你的異常過濾器不能有副作用;如果有,
則必須為你的終止處理函數(shù)保存它們。
2.8 版本2:未捕獲異常
將例程中的這行:
__except(filter(1, EXCEPTION_EXECUTE_HANDLER))
改為
__except(filter(1, EXCEPTION_CONTINUE_SEARCH))
于是沒有異常過濾器捕獲這個異常。執(zhí)行修改后的程序,你將看到:
0:before first try
1: try
2: try
3: try
4: try
4: raising exception
3: filter => EXCEPTION_CONTINUE_SEARCH
1: filter => EXCEPTION_CONTINUE_SEARCH
接著出現(xiàn)這個對話框:
1. 用戶異常對話框
點“Details”將其展開
2. 用戶異常對話框的詳細(xì)信息
在出錯信息中可看到:出錯程序是SEH_TEST,通過RaiseException 拋出的原始異常碼
是e0000001H。
這個異常漏出了程序,最后被操作系統(tǒng)捕獲和處理。有些象你的程序是這么寫的:
__try
{
int main(void)
{
...
}
}
__except(exception_dialog(), EXCEPTION_EXECUTE_HANDLER)
{
}
按對話框上的“Close”,所有的終止處理函數(shù)被執(zhí)行,并退棧,直到控制權(quán)回到捕獲異
常的處理函數(shù)。你可以明顯看到這些信息:
4: handling abnormal termination
2: handling abnormal termination
它們出現(xiàn)在關(guān)閉對話框之后。注意,你沒有看到:
0:continuation
因為它的實現(xiàn)代碼在終止處理函數(shù)之外,而退棧時只有終止處理函數(shù)被執(zhí)行。
對我們的試驗程序而言,捕獲異常的處理函數(shù)在main 之外,這意味著傳遞異常的行為
到了程序范圍外仍然在繼續(xù)。其結(jié)果是,程序被終止了。
2.9 版本3:恢復(fù)異常
接下來,改:
__except(except_filter(3, EXCEPTION_CONTINUE_SEARCH))
為:
__except(except_filter(3, EXCEPTION_CONTINUE_EXECUTION))
重新編譯并運行??梢钥吹竭@樣的輸出:
0:before first try
1: try
2: try
3: try
4: try
4: raising exception
3: filter => EXCEPTION_CONTINUE_EXECUTION
4: after exception
4: handling normal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
因為第三層的異常過濾器已經(jīng)捕獲了異常,第一層的過濾器不會被求值。捕獲異常的過
濾器求值為EXCEPTION_CONTINUE_EXECUTION,因此異常被恢復(fù)。異常處理函數(shù)不會被進入,
將從異常發(fā)生點正常執(zhí)行下去。
2.10 版本4:異常終止
這樣的結(jié)構(gòu):
__try
{
/* ... */
return;
}
或:
__try
{
/* ... */
goto label;
}
__finally
{
/* ... */
}
/* ... */
label:
被認(rèn)為是try 塊異常終止。以后調(diào)用AbnormalTermination()函數(shù)的話將返回非0 值,就象
異常仍然存在。
要看其效果,改這兩行:
trace(4, "raising exception");
RaiseException(exception_code, 0, 0, 0);
為:
trace(4, "exiting try block");
goto end_4;
第4 層的try 塊不是被一個異常結(jié)束的,現(xiàn)在是被goto 語句結(jié)束的。運行結(jié)果:
0:before first try
1: try
2: try
3: try
4: try
4: exiting try block
4: handling abnormal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
第4 層的終止處理函數(shù)認(rèn)為它正在處理異常終止,雖然并沒有發(fā)生過異常。(如果發(fā)生
過異常的話,我們至少能從一個異常過濾器的輸出信息上看出來的。)
結(jié)論:你不能只依賴AbnormalTermination()函數(shù)來判斷異常是否仍存在。
2.11 版本5:正常終止
如果想正常終止一個try 塊,也就是想要AbnormalTermination() 函數(shù)返回FALSE,應(yīng)
該使用Microsoft 特有的關(guān)鍵字__leave。想驗證的話,改:
goto end_4;
為:
__leave;
重新編譯并運行,結(jié)果是:
0:before first try
1: try
2: try
3: try
4: try
4: exiting try block
4: handling normal termination
3: continuation
2: continuation
2: handling normal termination
1: continuation
0:continuation
和版本4 的輸出非常接近,除了一點:第4 層的終止處理函數(shù)現(xiàn)在認(rèn)為它是在處理正常
結(jié)束。
2.12 版本6:隱式異常
前面的程序版本處理的都是用戶產(chǎn)生的異常。SEH 也可以處理Windows 自己拋出的異常。
改這行:
trace(4, "exiting try block");
__leave;
為:
trace(4, "implicitly raising exception");
*((char *) 0) = 'x';
這導(dǎo)致Windows 的內(nèi)存操作異常(引用空指針)。接著改:
__except(except_filter(3, EXCEPTION_CONTINUE_EXECUTION))
為:
__except(except_filter(3, EXCEPTION_EXECUTE_HANDLER))
以使程序捕獲并處理異常。
執(zhí)行結(jié)果為:
0:before first try
1: try
2: try
3: try
4: try
4: implicitly raising exception
3: filter => EXCEPTION_EXECUTE_HANDLER
4: handling abnormal termination
3: handling exception
2: continuation
2: handling normal termination
1: continuation
0:continuation
如我們所預(yù)料,Windows 在嵌套層次4 中觸發(fā)了一個異常,并被層次3 的異常處理函數(shù)
捕獲。
如果你想知道捕獲的精確異常碼,可以讓異常傳到main 外面去,就象版本2 中做的。
為此,改:
__except(except_filter(3, EXCEPTION_EXECUTE_HANDLER))
為:
__except(except_filter(3, EXCEPTION_CONTINUE_SEARCH))
結(jié)果對話框在按了“Details”后,顯示的信息非常象用戶異常。
圖3 內(nèi)存異常對話框
和版本2 的對話框不同是,上次顯示了特別的異常碼,這次說了“invalid page fault”
--更用戶友好些吧。
2.13 C++考慮事項
在所有C 兼容異常處理體系中,SEH 無疑是最完善和最靈活的(至少在Windows 環(huán)境下)。
具有諷刺意味的,它也是Windows 體系以外的環(huán)境中最不靈活的,它將你和特殊的運行平臺
及Visaul C++源碼兼容的編譯器牢牢綁在了一起。
如果只使用C 語言,并且不考慮移植到Windows 平臺以外,SEH 很好。但如果使用C++
并考慮可移植性,我強烈建議你使用標(biāo)準(zhǔn)C++異常處理而不用SEH。你可以在同一個程序中
同時使用SEH 和標(biāo)準(zhǔn)C++異常處理,只有一個限制:如果在有SEH try 塊的函數(shù)中定義了一
個對象,而這個對象又沒有non-trivial(無行為的)析構(gòu)函數(shù),編譯器會報錯。在同一函
數(shù)中同時使用這樣的對象和SEH 的__try,你必須禁掉標(biāo)準(zhǔn)C++異常處理。
(Visual C++默認(rèn)關(guān)掉標(biāo)準(zhǔn)C++異常處理。你可以使用命令行參數(shù)/GX 或Visual Studio
的Project Settings 對話框打開它。)
在以后的文章中,我會在討論標(biāo)準(zhǔn)C++異常處理時回顧SEH。我想將SEH 整合入C++的
主流中,通過將結(jié)構(gòu)化異常及Windows 運行庫支持映射為C++異常和標(biāo)準(zhǔn)C++運行庫支持。
2.14 MFC 異常處理
說明:這一節(jié)我需要預(yù)先引用一點點標(biāo)準(zhǔn)C++異常處理的知識,但要到下次才正式介紹
它們。這個提前引用是不可避免的,也是沒什么可驚訝的,因為Microsoft 將它們的MFC
異常的語法和語義構(gòu)建在標(biāo)準(zhǔn)C++異常的語法和語義的基礎(chǔ)上。
我到現(xiàn)在為止所講的異常處理方法對C 和C++都有效。在此之外,Microsoft 對C++程
序還有一個解決方案:MFC 異常處理類和宏。Microsoft 現(xiàn)在認(rèn)為MFC 異常處理體系過時了,
并鼓勵你盡可能使用標(biāo)準(zhǔn)C++異常處理。然而Visual C++仍然支持MFC 異常類和及宏,所以
我將給它個簡單介紹。
Microsoft 用標(biāo)準(zhǔn)C++異常實現(xiàn)了MFC3.0 及以后版本。所以你必須激活標(biāo)準(zhǔn)C++異常才
能使用MFC,即使你不打算顯式地使用這些異常。前面說過,你必須禁掉標(biāo)準(zhǔn)C++異常來使
用SEH,這也意味著你不能同時使用MFC 宏和SEH。Microsoft 明文規(guī)定這兩個異常體系是
互斥的,不能在同一程序中混合使用。
SEH 是擴展了編譯器關(guān)鍵字集,MFC 則定義了一組宏:
?? TRY
?? CATCH, AND_CATCH, 和END_CATCH
?? THROW 和 THROW_LAST
這些宏非常象C++的異常關(guān)鍵字try、catch 和throw。
另外,MFC 提供了異常類體系。所有名字為CXXXException 形式的類都是從抽象類
CException 派生的。這類似于標(biāo)準(zhǔn)C++運行庫在<setdxcept>中申明的從std::exception
開始的派生體系。但,標(biāo)準(zhǔn)C++的關(guān)鍵字可以處理絕大部分類型的異常對象,而MFC 宏只能
處理CException 的派生類型對象。
對于每個MFC 異常類CXXXException , 都有一個全局的輔助函數(shù)
AfxThrowXXXException() ,它構(gòu)造、初始化和拋出這個類的對象。你可以用這些輔助函數(shù)
處理預(yù)定義的異常類型,用THROW 處理自定義的對象(當(dāng)然,它們必須是從CException 派
生的)。
基本的設(shè)計原則是:
?? 用TRY 塊包含可能產(chǎn)生異常的代碼。
?? 用CATCH 檢測并處理異常。異常處理函數(shù)并不是真的捕獲對象,它們其實是捕獲了指向
異常的指針。MFC 靠動態(tài)類型來辨別異常對象。比較一下,SEH 靠運行時查詢異常碼來
辨別異常。
?? 可以在一個TRY 塊上捆綁多個異常處理函數(shù),每個捕獲一個C++靜態(tài)類型不同的對象。
第一個處理函數(shù)使用宏CATCH,以后的使用AND_CATCH,用END_CATCH 結(jié)束處理函數(shù)隊
列。
?? MFC 自己可能觸發(fā)異常,你也可以顯式觸發(fā)異常(通過THROW 或MFC 輔助函數(shù))。在異
常處理函數(shù)內(nèi)部,可以用THROW_LAST 再次拋出最近一次捕獲的異常。
?? 異常一被觸發(fā),異常處理函數(shù)就將被從里到外進行搜索,和SEH 時一樣。搜索停止于找
到一個類型匹配的異常處理函數(shù)。所有異常都是終止。和SEH 不一樣,MFC 沒有終止處
理函數(shù),你必須依賴于局部對象的析構(gòu)函數(shù)。
一個小MFC 例子,將大部分題目都包括了:
#include <stdio.h>
#include "afxwin.h"
void f()
{
TRY
{
printf("raising memory exception ");
AfxThrowMemoryException();
printf("this line should never appear ");
}
CATCH(CException, e)
{
printf("caught generic exception; rethrowing ");
THROW_LAST();
printf("this line should never appear ");
}
END_CATCH
printf("this line should never appear ");
}
int main()
{
TRY
{
f();
printf("this line should never appear ");
}
CATCH(CFileException, e)
{
printf("caught file exception ");
}
AND_CATCH(CMemoryException, e)
{
printf("caught memory exception ");
}
/* ... handlers for other CException-derived types ... */
AND_CATCH(CException, e)
{
printf("caught generic exception ");
}
END_CATCH
return 0;
}
/*
When run yields
raising memory exception
caught generic exception; rethrowing
caught memory exception
*/
記住,異常處理函數(shù)捕獲指向?qū)ο蟮闹羔槪皇菍嶋H的對象。所以,處理函數(shù):
CATCH(CException, e)
{
// ...
}
定義了一個局部指針CException *e 指向了被拋出的異常對象。基于C++的多態(tài),這個指針
可以引用任何從CException 派生的對象。
如果同一try 塊有多個處理函數(shù),它們按從上到下的順序進行匹配搜索的。所以,你應(yīng)
該將處理最派生類的對象的處理函數(shù)放在前面,不然的話,更派生類的處理函數(shù)不會接收任
何異常的(再次拜多態(tài)所賜)。
因為你典型地想捕獲CException,MFC 定義了幾個CException 特有宏:
?? CATCH_ALL(e) 和AND_CATCH_ALL(e) , 等價于CATCH(CException, e) 和
AND_CATCH(CException, e)。
?? END_CATCH_ALL ,結(jié)束CATCH_ALL... AND_CATCH_ALL 隊列。
?? END_TRY 等價于CATCH_ALL(e);END_CATCH_ALL。這讓TRY... END_TRY 中沒有處理函數(shù)
或說是接收所有拋出的異常。
這個被指的異常對象由MFC 隱式析構(gòu)和歸還內(nèi)存。這一點和標(biāo)準(zhǔn)C++異常處理函數(shù)不一
樣,MFC 異常處理不會讓任何人取得被捕獲的指針的所有權(quán)。因此,你不能用MFC 和標(biāo)準(zhǔn)C++
體系同時處理相同的異常對象;不然的話,將導(dǎo)致內(nèi)存泄漏:引用已被析構(gòu)的對象,并重復(fù)
析構(gòu)和歸還同一對象。
2.15 小結(jié)
MSDN 在線還有另外幾篇探索結(jié)構(gòu)化異常處理和MFC 異常宏的文章。
下次我將介紹標(biāo)準(zhǔn)C++異常,概述它們的特點及基本原理。我還會將它們和到現(xiàn)在已經(jīng)
看到的方法進行比較。
回到目錄
3. 標(biāo)準(zhǔn)C++異常處理的基本語法和語義
這次,我來概述標(biāo)準(zhǔn)C++異常處理的基本語法和語義。順便,我會將它和前兩次提到的
技術(shù)進行比較。(在本文及以后,我將標(biāo)準(zhǔn)C++異常處理簡稱為EH,將微軟的方法稱為SEH。)
3.1 基本語法和語義
EH 引入了3 個新的C++語言關(guān)鍵字:
?? catch
?? throw
?? try
異常通過如下語句觸發(fā)
throw [expression]
函數(shù)通過“異常規(guī)格申明”定義它將拋出什么異常:
throw([type-ID-list])
可選項type-ID-list 包含一個或多個類型的名字,以逗號分隔。這些異??縯ry 塊中
的異常處理函數(shù)進行捕獲。
try compound-statement handler-sequence
處理函數(shù)隊列包含一個或多個處理函數(shù),形式如下:
catch ( exception-declaration ) compound-statement
處理函數(shù)的“異常申明”指明了這個函數(shù)將捕獲什么類型的異常。
和SEH 一樣,跟在try 和catch 后面的語句必須刮在{}內(nèi),而整個try 塊組成一條完整
的大語句。
例子:
void f() throw(int, some_class_type)
{
int i;
// ... generate an 'int' exception
throw i;
// ...
}
int main()
{
try
{
f();
}
catch(int e)
{
// ... handle 'int' exception ...
}
catch(some_class_type e)
{
// ... handle 'some_class_type' exception ...
}
// ... possibly other handlers ...
return 0;
}
異常規(guī)格申明是EH 特有的,SEH 和MFC 都沒有類似的東西。一個空的異常規(guī)格申明表
明函數(shù)不拋出任何異常:
void f() throw()
{
// ... function throws no exceptions ...
}
如果函數(shù)沒有異常規(guī)格申明,它可以拋出任何類型的異常:
void f()
{
// ... function can throw anything or nothing ...
}
當(dāng)函數(shù)拋異常時,關(guān)鍵字throw 通常后面帶一個被拋出的對象:
throw i;
然而,throw 也可以不帶對象:
catch(int e)
{
// ... handle 'int' exception ...
throw;
}
它的效果是再次拋出當(dāng)前正被捕獲的對象(int e)。因為空throw 的作用是再次拋出
已存在的異常對象,所以它必須位于catch 語句塊中。MFC 也有再次拋出異常的功能,SEH
則沒有,它沒有將異常對象交給過處理函數(shù),所以沒什么可再次拋出的。
就象函數(shù)原型中的參數(shù)申明一樣,異常申明也可以是無名的:
catch(char *)
{
// ... handle 'char *' exception ...
}
當(dāng)這個處理函數(shù)捕獲一個char *型的異常對象時,它不能操作這個對象,因為這個對
象沒有名字。
異常申明還可以是這樣的特殊形式:
catch(...)
{
// ... handle any type of exception ...
}
就象不定參數(shù)中的“...”一樣,異常申明中的“...”可以匹配任何異常的類型。
3.2 標(biāo)準(zhǔn)異常對象的類型
標(biāo)準(zhǔn)庫函數(shù)可能報告錯誤。在C 標(biāo)準(zhǔn)庫中的報錯方式在前面說過了。在C++標(biāo)準(zhǔn)庫中,
有些函數(shù)拋出特定的異常,而另外一些根本不拋任何異常。
因為C++標(biāo)準(zhǔn)中沒有明確規(guī)定,所以C++的庫函數(shù)可以拋出任何對象或不拋。但C++標(biāo)
準(zhǔn)推薦運行庫的實現(xiàn)通過拋出定義在<stdexecpt>中的異常類型或其派生類型來報告錯誤:
namespace std
{
class logic_error; // : public exception
class domain_error; // : public logic_error
class invalid_argument; // : public logic_error
class length_error; // : public logic_error
class out_of_range; // : public logic_error
class runtime_error; // : public exception
class range_error; // : public runtime_error
class overflow_error; // : public runtime_error
class underflow_error; // : public runtime_error
}
這些(異常)類只對C++標(biāo)準(zhǔn)庫有約束力。在你自己的代碼中,你可以拋出(和捕獲)
任何你所象要的類型。
3.3 標(biāo)準(zhǔn)中的其它申明
標(biāo)準(zhǔn)庫頭文件<exception>申明了幾個EH 類型和函數(shù)
namespace std
{
//
// types
//
class bad_exception;
class exception;
typedef void (*terminate_handler)();
typedef void (*unexpected_handler)();
//
// functions
//
terminate_handler set_terminate(terminate_handler) throw();
unexpected_handler set_unexpected(unexpected_handler) throw();
void terminate();
void unexpected();
bool uncaught_exception();
}
提要:
?? exception 是所有標(biāo)準(zhǔn)庫拋出的異常的基類。
?? uncaught_exception()函數(shù)在有異常被拋出卻沒有被捕獲時返回true,其它情況返回
false。它類似于SEH 的函數(shù)AbnormalTermination()。
?? terminate()是EH 的應(yīng)急處理。它在異常處理體系陷入了不可恢復(fù)狀態(tài)時被調(diào)用,經(jīng)常
是因為試圖重入(在前一個異常正處理過程中又拋了一個異常)。
?? unexpected()在函數(shù)拋出一個它沒有在“異常規(guī)格申明”中申明的異常時被調(diào)用。這個
預(yù)料外的異??赡茉谕藯_^程中被替換為一個bad_excetion 對象。
?? 運行庫提供了缺省terminate_handler()和unexpected_handler() 函數(shù)處理對應(yīng)的情
況。你可以通過set_terminate()和set_unexpected()函數(shù)替換庫的默認(rèn)版本。
3.4 異常生命期
EH 運行于異常生命期的五個階段:
?? 程序或運行庫遇到一個錯誤狀況(階段1)并且拋出一個異常(階段2)。
?? 程序的運行停止于異常點,開始搜索異常處理函數(shù)。搜索沿調(diào)用棧向上搜索(很象SEH
終止異常時的行為)。
?? 搜索結(jié)束于找到了一個異常申明與異常對象的靜態(tài)類型相匹配(階段3)。于是進入相
應(yīng)的異常處理函數(shù)。
?? 異常處理函數(shù)結(jié)束后,跳到此異常處理函數(shù)所在的try 塊下面最近的一條語句開始執(zhí)行
(階段5)。這個行為意味著C++標(biāo)準(zhǔn)中異??偸墙K止。
這些步驟演示于這個簡單的例子中:
#include <stdio.h>
static void f(int n)
{
if (n != 0) // Stage 1
throw 123; // Stage 2
}
extern int main()
{
try
{
f(1);
printf("resuming, should never appear ");
}
catch(int) // Stage 3
{
// Stage 4
printf("caught 'int' exception ");
}
catch(char *) // Stage 3
{
// Stage 4
printf("caught 'char *' exception ");
}
catch(...) // Stage 3
{
// Stage 4
printf("caught typeless exception ");
}
// Stage 5
printf("terminating, after 'try' block ");
return 0;
}
/*
When run yields
caught 'int' exception
terminating, after 'try' block
*/
3.5 基本原理
C 標(biāo)準(zhǔn)庫的異常體系處理C++語言時有如下難題:
?? 析構(gòu)函數(shù)被忽略。既然C 標(biāo)準(zhǔn)庫異常體系是為C 語言設(shè)計的,它們不知道C++的析構(gòu)函
數(shù)。尤其,abort()、exit()和longjmp()在退?;虺绦蚪K止時不調(diào)用局部對象的析構(gòu)
函數(shù)。
?? 繁瑣的。查詢?nèi)謱ο蠡蚝瘮?shù)返回值導(dǎo)致了代碼混亂-你必須在所有可能發(fā)生異常的地
方進行明確的異常情況檢測,即使是異常情況可能實際上從不發(fā)生。因為這種方法是如
此繁瑣,程序員們可能會故意“忘了”檢測異常情況。
?? 無彈性的。Longjmp()“拋出”的只能是簡單的int 型。errno 和signal()/raise()只
使用了很小的一個值域集合,分辨率很低。Abort()和exit()總是終止程序。Assert()
只工作在debug 版本中。
?? 非固有的。所有的C 標(biāo)準(zhǔn)庫異常體系都需要運行庫的支持,它不是語言內(nèi)核支持的。
微軟特有的異常處理體系也不是沒有限制的:
?? SEH 異常處理函數(shù)不是直接捕獲一個異常對象,而是通過查詢一個(概念性的)類似
errno 的全局值來判斷什么異常發(fā)生了。
?? SEH 異常處理函數(shù)不能組合,給定try 塊的唯有的一個處理函數(shù)必須在運行期識別和處
理所有的異常事件。
?? MFC 異常處理函數(shù)只能捕獲CException 及派生類型的指針。
?? 通過包含定義了MFC 異常處理函數(shù)的宏的頭文件,程序包含了數(shù)百個無關(guān)的宏和申明。
?? MFC 和SEH 都是專屬于與Microsoft 兼容的開發(fā)環(huán)境和Windows 運行平臺的。
標(biāo)準(zhǔn)C++異常處理避免了這些短處:
?? 析構(gòu)安全。在拋異常而進行退棧時,局部對象的析構(gòu)函數(shù)被按正確的順序調(diào)用。
?? 不引人注目的。異常的捕獲是暗地里的和自動的。程序員無需因錯誤檢測而搞亂設(shè)計。
?? 精確的。因為幾乎任何對象都可以被拋出和捕獲,程序員可以控制異常的內(nèi)容和含義。
?? 可伸縮的。每個函數(shù)可以有多個try 塊。每個try 塊可以有單個或一組處理函數(shù)。每個
處理函數(shù)可以捕獲單個類型,一組類型或所有類型的異常。
?? 可預(yù)測的。函數(shù)可以指定它們將拋的異常類型,異常處理函數(shù)可以指定它們捕獲什么類
型的異常。如果程序違反了其申明,標(biāo)準(zhǔn)庫將按可預(yù)測的、用戶定義的方式運行。
?? 固有的。EH 是C++語言的一部分。你可以定義、throw 和catch 異常而不需要包含任何
庫。
?? 標(biāo)準(zhǔn)的。EH 在所有的標(biāo)準(zhǔn)C++的實現(xiàn)中都可用。
基于更完備的想法,C++標(biāo)準(zhǔn)委員會考慮過兩個EH 的設(shè)計,在D&E 的16 章。(For a more
complete rationale, including alternative EH designs considered by the C++
Standard's committee, check out Chapter 16 of the D&E.)
3.6 小結(jié)
下次,我將更深入挖掘EH 的語言核心特性和EH 的標(biāo)準(zhǔn)庫支持。我也將展示Microsoft
Visual C++實現(xiàn)EH 的內(nèi)幕。我將開始標(biāo)志出EH 的那些Visual C++只部分支持或完全不支
持的特性,并且尋找繞過這些限制的方法。
在我相信設(shè)計EH 的基本原理是健全的的同時,我也認(rèn)為EH 無意中包含了一些嚴(yán)重的后
果。不用責(zé)備C++標(biāo)準(zhǔn)的制訂者的短視,我理解設(shè)計和實現(xiàn)有效的異常處理是多么的難。當(dāng)
我們遭遇到這些無意中的后果時,我將展示它們對你代碼的微妙影響,并且推薦一些技巧來
減輕其影響。
回到目錄
4. 實例剖析EH
到現(xiàn)在為止,我仍然逗留在C 和C++的范圍內(nèi),但這次要稍微涉及一下匯編語言。目標(biāo):
初步揭示Visual C++對EH 的throw 和catch 的實現(xiàn)。本文不是巨細(xì)無遺的,畢竟我的原則
是只關(guān)注(C/C++)語言本身。然而,簡單的揭示EH 的實現(xiàn)對理解和信任EH 大有幫助。
4.1 我們所害怕的唯一一件事
在throw 過程中退棧時,EH 追蹤哪個局部對象需要析構(gòu),預(yù)先安排必須的析構(gòu)函數(shù)的
調(diào)用,并且將控制權(quán)交給正確的異常處理函數(shù)。為了完成EH 所需的記錄和管理工作,編譯
器暗中在生成的代碼中注入了數(shù)據(jù)、指令和庫引用。
不幸的是,很多程序員(以及他們的經(jīng)理)討厭這種注入行為導(dǎo)致過分的代碼膨脹。他
們感到恐慌,認(rèn)為EH 會削弱程序的使用價值。所以,我認(rèn)為EH 觸及了人們對未知的恐懼:
因為源碼中沒有明確地表露出EH 的工作,他們將作最壞的估算。
為了戰(zhàn)勝這種恐懼,讓我們通過短小的Visual C++代碼剖析EH。
4.2 例1:基線版本
生成一個新的C++源文件EH.cpp 如下:
class C
{
public:
C()
{
}
~C()
{
}
};
void f1()
{
C x1;
}
int main()
{
f1();
return 0;
}
然后,創(chuàng)建一個新的Visual C++控制臺項目,并包含EH.CPP 為唯一的源文件。使用默
認(rèn)項目屬性,但打開“生成源碼/匯編混合的.asm 文件”選項。編譯出Debug 版本。在我機
器上,得到的EH.exe 是23,040 字節(jié)。
打開EH.asm 文件,你將發(fā)現(xiàn)f1()函數(shù)非常接近預(yù)料:設(shè)置??蚣埽{(diào)用xl 的構(gòu)造和
析構(gòu)函數(shù),然后重設(shè)??蚣?。特別地,你將注意到?jīng)]有任何EH 產(chǎn)物或記錄――并不奇怪,
因為程序沒有拋出或捕獲任何異常。
4.3 例2:單異常處理函數(shù)
現(xiàn)在將f1 改為如下形式:
void f1()
{
C x1;
try
{
}
catch(char)
{
}
}
重新編譯EH.exe,然后注意文件大小。在我機器上,大小從23,040 字節(jié)增到29,696
字節(jié)。有些心跳吧,EH 導(dǎo)致了29%的文件大小的增加。但看一下絕對增加,才6,656 字節(jié),
并且絕大部分是來自于固定大小的庫開銷。剩下的少量才是額外注入到EH.obj 中的代碼和
數(shù)據(jù)。
在EH.asm 中,可以找到符號__$EHRec$定義了一個常量值,它表示對于??蚣艿钠屏?。
每個函數(shù)都在其生成的代碼中引用了__$EHRec$,編譯器暗中定義了一個局部的“EH 記錄”
記錄對象。
EH 記錄是暫時的:和需要在代碼中有個永久的靜態(tài)記錄相比,它們存在于棧中,在函
數(shù)被進入時產(chǎn)生,在函數(shù)退出是消失。在且僅在函數(shù)需要提早析構(gòu)局部對象時,編譯器增加
了EH 記錄(并且由局部代碼維護它)。
隱含意思是,有些函數(shù)不需要EH 記錄??催@個,增加的第二個函數(shù):
void f2()
{
}
沒有涉及對象和異常。重新編譯程序。EH.asm 顯示f1()的棧中和以前一樣包括一個EH 記錄,
但f2()的棧中沒有。然而,如果將代碼改成這樣:
void f2()
{
C x2;
f1();
}
f2()現(xiàn)在定義了一個局部的EH 記錄,即使f2()自己沒有try 塊。為什么?因為f2()
調(diào)用了f1(),而f1()可能拋出異常而終止f2(),因此需要提早析構(gòu)x2。
結(jié)論:如果一個包含局部對象的函數(shù)沒有明確處理異常,但可能傳遞一個別人拋的異常,
那么函數(shù)仍然需要一個EH 記錄和相應(yīng)的維護代碼。
這使你苦惱了嗎?只要短路異常鏈就可以了。在我們的例子中,將f1()的定義改成:
void f1() throw()
{
C x1;
try
{
}
catch(char)
{
}
}
現(xiàn)在f1()承諾不拋異常。結(jié)果,f2()不需要傳遞f1()的異常,也就不需要EH 記錄了。
你可以重新編譯程序來核實,查看EH.asm 并發(fā)現(xiàn)f2()的代碼不再提到__$EHRec$。
4.4 例3:多個異常處理函數(shù)
EH 記錄及其支撐代碼不是編譯所引入的唯有的記錄。對給定try 塊的每個處理函數(shù),
編譯器也都創(chuàng)建了入口表。想看得清楚些,將現(xiàn)在的EH.asm 改名另存,并將f1()擴展為:
void f1() throw()
{
C x1;
try
{
}
catch(char)
{
}
catch(int)
{
}
catch(long)
{
}
catch(unsigned)
{
}
}
重新編譯,然后比較兩次的EH.asm。
(提醒:下面列出的EH.asm,我沒有忽略不相關(guān)的東西,也沒有用省略號代替什么。
精確的標(biāo)號名在你的系統(tǒng)上可能不一樣。并且不要以匯編語言分析器的眼光看這些代碼。)
在我的EH.asm 中,相關(guān)的名字、描述符和注釋如下:
PUBLIC ??_R0D@8 ; char `RTTI Type Descriptor'
PUBLIC ??_R0H@8 ; int `RTTI Type Descriptor'
PUBLIC ??_R0J@8 ; long `RTTI Type Descriptor'
PUBLIC ??_R0I@8 ; unsigned int `RTTI Type Descriptor'
_DATA SEGMENT
??_R0D@8 DD FLAT:??_7type_info@@6B@ ; char `RTTI Type Descriptor'
DD ...
DB '.D', ...
_DATA ENDS
_DATA SEGMENT
??_R0H@8 DD FLAT:??_7type_info@@6B@ ; int `RTTI Type Descriptor'
DD ...
DB '.H', ...
_DATA ENDS
_DATA SEGMENT
??_R0J@8 DD FLAT:??_7type_info@@6B@ ; long `RTTI Type Descriptor'
DD ...
DB '.J', ...
_DATA ENDS
_DATA SEGMENT
??_R0I@8 DD FLAT:??_7type_info@@6B@ ; unsigned int `RTTI Type Descriptor'
DD ...
DB '.I', ...
_DATA ENDS
(對于“RTTI Type Descriptor”和“type_info”的注釋提示我,Visual C++在EH
和RTTI 時使用了同樣的類型名描述符。)
編譯器同樣生成了對在xdata@x 段中定義的類型描述符的引用。每個類型對應(yīng)一個捕獲
這種類型的異常處理函數(shù)的地址。這種描述符/處理函數(shù)對構(gòu)成了EH 庫代碼分發(fā)異常時的分
發(fā)表。這些也是從我的EH.asm 下摘抄的,加上了注釋和圖表:
xdata$x SEGMENT
$T214 DD ...
DD ...
DD FLAT:$T217 ;---+
DD ... ; |
DD FLAT:$T218 ;---|---+
DD 2 DUP(...) ; | |
ORG $+4 ; | |
; | |
$T217 DD ... ;<--+ |
DD ... ; |
DD ... ; |
DD ... ; |
; |
$T218 DD ... ;<------+
DD ...
DD ...
DD 04H ; # of handlers
DD FLAT:$T219 ;---+
ORG $+4 ; |
; |
$T219 DD ... ;<--+
DD FLAT:??_R0D@8 ; char RTTI Type Descriptor
DD ...
DD FLAT:$L206 ; catch(char) address
DD ...
DD FLAT:??_R0H@8 ; int RTTI Type Descriptor
DD ...
DD FLAT:$L207 ; catch(int) address
DD ...
DD FLAT:??_R0J@8 ; long RTTI Type Descriptor
DD ...
DD FLAT:$L208 ; catch(long) address
DD ...
DD FLAT:??_R0I@8 ; unsigned int RTTI Type Descriptor
DD ...
DD FLAT:$L209 ; catch(unsigned int) address
xdata$x ENDS
分發(fā)表表頭(標(biāo)號$T214、 $T217 和 $T218 處的代碼)是f1()專屬的,并為f1()的所
有異常處理函數(shù)共享。$T219 出的分發(fā)表的每一個入口項都特屬于f1()的一個特定的異常處
理函數(shù)。
更一般地,編譯器為每一帶try 塊的函數(shù)生成一個分發(fā)表表頭,為每一個異常處理函數(shù)
增加一個入口項。類型描述符為程序的所有分發(fā)表共享。(例如,程序中所有catch(long)
的處理函數(shù)引用同樣的??_R0J@8 類型描述符。)
提要:要減小EH 的空間開銷,應(yīng)該將程序中捕獲異常的函數(shù)數(shù)目減到最小,將函數(shù)中
異常處理函數(shù)的數(shù)目減到最小,將異常處理函數(shù)所捕獲的異常類型減到最小。
4.5 例四:拋異常
用“拋一個異?!眮韺⑺袞|西融會起來。將f1()的try 語句改成這樣:
try
{
throw 123; // type 'int' exception
}
重新編譯程序,打開EH.asm,注意新出現(xiàn)的東西(我同樣加了的注釋和圖表)。
; in these exported names, 'H' is the RTTI Type Descriptor
; code for 'int' -- which matches the data type of
; the thrown exception value 123
PUBLIC __TI1H
PUBLIC __CTA1H
PUBLIC __CT??_R0H@84
; EH library routine that actually throws exceptions
EXTRN __CxxThrowException@8:NEAR
; new static data blocks used by library
; when throwing 'int' exception
xdata$x SEGMENT
__CT??_R0H@84 DD ... ;<------+
DD FLAT:??_R0H@8 ; | ??_R0H@8 is RTTI 'int'
; | Type Descriptor
DD ... ; |
DD ... ; |
ORG $+4 ; |
DD ... ; |
DD ... ; |
; |
__CTA1H DD ... ;<--+ |
DD FLAT:__CT??_R0H@84 ;---|---+
; |
__TI1H DD ... ; | __TI1H is argument passed to
DD ... ; | __CxxThrowException@8
DD ... ; |
DD FLAT:__CTA1H ;---+
xdata$x ENDS
和類型描述符一樣,這些新的數(shù)據(jù)塊為全部程序共享,例如,所有拋int 異常代碼引用
__TI1H. 。同樣要注意:相同的類型描述符被異常處理函數(shù)和throw 語句引用。
翻到f1()處,相關(guān)部分如下:
;void f1() throw()
; {
; try
; {
...
push $L224 ; Address of code to adjust stack frame via handler
; dispatch table. Invoked by __CxxThrowException@8.
...
; throw 123;
push OFFSET FLAT:__TI1H ; Address of data area diagramed
; above
mov DWORD PTR $T213[ebp], 123 ; 123 is the exception's value
lea eax, DWORD PTR $T213[ebp]
push eax
call __CxxThrowException@8 ; Call into EH library, which in
; turn eventually calls $L224
; and $L216 a.k.a. 'catch(int)'
; }
; // ...
; catch(int)
$L216:
; {
mov eax, $L182 ; Return to EH library, which jumps to $L182
ret 0
; }
; // ...
$L182:
; // Call local-object destructors, clean up stack, return
; }
$L224: ; This label referenced by 'try' code.
mov eax, OFFSET FLAT:$T223 ; $T223 is handler dispatch table, what
; had previously been label $T214
; before we added 'throw 123'
jmp ___CxxFrameHandler ; internal library routine
當(dāng)程序運行時,__CxxThrowException@8(EH 的庫函數(shù))調(diào)用了$L216,catch(int)處
理函數(shù)的地址。當(dāng)處理函數(shù)一結(jié)束,程序就繼續(xù)順EH 庫中的代碼向下運行,跳到$L224,繼
續(xù)向下并最終跳到$L182。這個標(biāo)號是f1()的終止和cleanup 代碼的地址,在其中調(diào)用了x1
的析構(gòu)函數(shù)。你可以在調(diào)試器下用單步進行驗證。
4.6 小結(jié)
所有的異常處理體系都導(dǎo)致開銷。除非你愿意在沒有任何異常安全體系的情況下執(zhí)行代
碼,你必須同意付出速度和空間的代價。EH 作為語言的特性有優(yōu)點的:編譯器明確知道EH
的實現(xiàn)并可以據(jù)此優(yōu)化它。
除了編譯器的優(yōu)化,你自己還有很多方法來優(yōu)化。在以后的文章中,我將揭示特定的方
法來將EH 的代價減到最小。有些方法是基于標(biāo)準(zhǔn)C++的,其它則依賴于Visual C++的具體
實現(xiàn)。
回到目錄
5. C++的new 和delete 操作時的異常處理
今天,我們開始學(xué)習(xí)C++的new 和delete 操作時的異常處理。首先,我將介紹標(biāo)準(zhǔn)C++
運行庫對new 和delete 操作的支持。然后,介紹伴隨著這些支持的異常。
5.1 New 和Delete 表達(dá)式
當(dāng)寫
B *p = new D;
這里,B 和D 是class 類型,并且有構(gòu)造和析構(gòu)函數(shù),編譯器實際產(chǎn)生的代碼大約是這樣的:
B *p = operator new(sizeof(D));
D::D(p);
過程是:
?? new 操作接受D 對象的大?。ㄗ止?jié)為單位)作為參數(shù)。
?? new 操作返回一塊大小足以容納一個D 對象的內(nèi)存的地址。
?? D 的缺省構(gòu)造函數(shù)被調(diào)用。這個構(gòu)造函數(shù)傳入的this 指針就是剛剛返回的內(nèi)存地址。
?? 最終結(jié)果:*p 是個完整構(gòu)造了的對象,靜態(tài)類型是B,動態(tài)類型是D。
相似的,語句
delete p;
差不多被編譯為
D::~D(p);
operator delete(p);
D 的析構(gòu)函數(shù)被調(diào)用,被傳入的this 指針是p;然后delete 操作釋放被分配的內(nèi)存。
new 操作和delete 操作其實是函數(shù)。如果你沒有提供自己的版本,編譯器會使用標(biāo)準(zhǔn)
C++運行庫頭文件<new>中申明的版本:
void *operator new(std::size_t);
void operator delete(void *);
和其它標(biāo)準(zhǔn)運行庫函數(shù)不同,它們不在命名空間std 內(nèi)。
因為編譯器隱含地調(diào)用這些函數(shù),所以它必須知道如何尋找它們。如果編譯器將它們放
在特別的空間內(nèi)(如命名空間std),你就無法申明自己的替代版本了。因此,編譯器按絕
對名字從里向外進行搜索。如果你沒有申明自己的版本,編譯器最終將找到在<new>中申明
的全局版本。
這個頭文件包含了8 個new/delete 函數(shù):
//
// new and delete
//
void *operator new(std::size_t);
void delete(void *);
//
// array new and delete
//
void *operator new[](std::size_t);
void delete[](void *);
//
// placement new and delete
//
void *operator new(std::size_t, void *);
void operator delete[](void *, void *);
//
// placement array new and delete
//
void *operator new[](std::size_t, void *);
void operator delete[](void *, void *);
前兩個我已經(jīng)介紹了。接下來兩個分配和釋放數(shù)組對象,而最后四個根本不分配和釋放
任何東西!
5.2 數(shù)組new 和數(shù)組delete
new[]操作被這樣的表達(dá)式隱含調(diào)用:
B *p = new D[N];
編譯器對此的實現(xiàn)是:
B *p = operator new[](sizeof(D) * N + _v);
for (std::size_t _i(0); _i < N; ++_i)
D::D(&p[_i]);
前一個例子分配和構(gòu)造單個D 對象,這個例子分配和構(gòu)造一個有N 個D 對象的數(shù)組。注
意,傳給new[]操作的字節(jié)大小是sizeof(D)*N + _v,所有對象的總大小加_v。在這里, _v
是數(shù)組分配時的額外開銷。
如你所想,
delete[] p;
實現(xiàn)為:
for (std::size_t _i(_N_of(p)); _i > 0; --_i)
D::~D(&p[i-1]);
operator delete[](p);
這里,_N_of(p)是個假想詞,它依賴于你的編譯器在檢測*p 中的元素個數(shù)時的實現(xiàn)體
系。
和p = new D[N]不同(它明確說明了*p 包含N 個元素),delete[] p 沒有在編譯期明
確說明*p 元素個數(shù)。你的程序必須在運行期推算元素個數(shù)。C++標(biāo)準(zhǔn)沒有強制規(guī)定推算的實
現(xiàn)體系,而我所見過的編譯器共有兩種實現(xiàn)方法:
?? 在*p 前面的字節(jié)中保存元素個數(shù)。其存儲空間來自于new[]操作時_v 字節(jié)的額外開銷。
?? 由標(biāo)準(zhǔn)運行庫維護一個私有的N 對p 的映射表。
5.3 Placement New 和 Placement Delete
關(guān)鍵字new 可以接受參數(shù):
p = new(arg1, arg2, arg3) D;
(C++標(biāo)準(zhǔn)稱這樣的表達(dá)式為 “new with placement”或“placement new”,我馬上會
簡單地解釋原因。)這些參數(shù)會被隱含地傳給new 操作函數(shù):
p = operator new(sizeof(D), arg1, arg2, arg3);
注意,第一個參數(shù)仍然是要生成對象的字節(jié)數(shù),其它參數(shù)總是跟在它后面。
標(biāo)準(zhǔn)運行庫定義了一個new 操作的特別重載版本,它接受一個額外參數(shù):
void *operator new(std::size_t, void *);
這種形式的new 操作被如下的語句隱含調(diào)用:
p = new(addr) D;
這里,addr 是某些數(shù)據(jù)區(qū)的地址,并且類型兼容于void *。
addr 傳給這個特別的new 操作,這個特別的new 操作和其它new 操作一樣返回將被構(gòu)
造的內(nèi)存的地址,但不需要在自由內(nèi)存區(qū)中再申請內(nèi)存,它直接將addr 返回:
void *operator new(std::size_t, void *addr)
{
return addr;
}
這個返回值然后被傳給D::D 作構(gòu)造函數(shù)的this 指針。
就這樣,表達(dá)式
p = new(addr) D;
在addr 所指的內(nèi)存上構(gòu)造了一個D 對象,并將p 賦為addr 的值。這個方法讓你有效地指定
新生成對象的位置,所以被叫作“placement new”。
這個new 的額外參數(shù)形式最初被設(shè)計為控制對象的位置的,但是C++標(biāo)準(zhǔn)委員會認(rèn)識到
這樣的傳參體系可以被用于任意用途而不僅是控制對象的位置。不幸的是,術(shù)語“placement”
已經(jīng)被根據(jù)最初目的而制訂,并適用于所有new 操作的額外參數(shù)的形式,即使它們根本不試
圖控制對象的位置。
所以,下面每個表達(dá)式都是placement new 的一個例子:
new(addr) D; // calls operator new(std::size_t, void *)
new(addr, 3) D; // calls operator new(std::size_t, void *, int)
new(3) D; // calls operator new(std::size_t, int)
即使只有第一個形式是一般被用作控制對象位置的。
5.4 placement Delete
現(xiàn)在,只要認(rèn)為 placement delete 是有用處的就行了。我肯定會講述理由的,可能就
在接下來的兩篇內(nèi)。
Placement new 操作和placement delete 操作必須成對出現(xiàn)。一般來說,每一個
void *operator new(std::size_t, p1, p2, p3, ..., pN);
都對應(yīng)一個
void operator delete(void *, p1, p2, p3, ..., pN);
根據(jù)這條原則,標(biāo)準(zhǔn)運行庫定義了
void operator delete(void *, void *);
以對應(yīng)我剛講的placement new 操作。
5.5 數(shù)組New 和數(shù)組Delete
基于對稱,標(biāo)準(zhǔn)運行庫也申明了placement new[]操作和placement delete[]操作:
void *operator new[](std::size_t, void *);
void operator delete[](void *, void *);
如你所料:placement new[]操作返回傳入的地址,而placement delete[]操作的行為
和我沒有細(xì)述的placement delete 操作行為幾乎一樣。
5.6 異常
現(xiàn)在,我們把這些new/delete 和異常結(jié)合起來。再次考慮這條語句:
B *p = new D;
當(dāng)其調(diào)用new 操作而沒有分配到足夠內(nèi)存時將發(fā)生什么?
在C++的黑暗年代(1994 年及以前),對大部分編譯器而言,new 操作將返回NULL。這
曾經(jīng)是對C 的malloc 函數(shù)的合理擴展。幸運的是,我們現(xiàn)在生活在光明的年代,編譯器強
大了,類被設(shè)計得很漂亮,而編譯運行庫的new 操作會拋異常了。
前面,我展示了在<new>中出現(xiàn)的8 個函數(shù)的申明。那時,我做了些小手腳;這里是它
們的完整形式:
namespace std
{
class bad_alloc
{
// ...
};
}
//
// new and delete
//
void *operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
//
// array new and delete
//
void *operator new[](std::size_t) throw(std::bad_alloc);
void operator delete[](void *) throw();
//
// placement new and delete
//
void *operator new(std::size_t, void *) throw();
void operator delete(void *, void *) throw();
//
// placement array new and delete
//
void *operator new[](std::size_t, void *) throw();
void operator delete[](void *, void *) throw();
在這些new 操作族中,只有非placement 形式的會拋異常(std::bad_alloc)。這個異
常意味著內(nèi)存耗盡狀態(tài),或其它內(nèi)存分配失敗。你可能奇怪為什么placement 形式不拋異常;
但記住,這些函數(shù)實際上根本不分配任何內(nèi)存,所以它們沒有分配問題可報告。
沒有delete 操作拋異常。這不奇怪,因為delete 不分配新內(nèi)存,只是將舊內(nèi)存還回去。
5.7 異常消除
相對于會拋異常的new 操作形式,<new>中也申明了不拋異常的重載版本:
namespace std
{
struct nothrow_t
{
// ...
};
extern const nothrow_t nothrow;
}
//
// new and delete
//
void *operator new(std::size_t, std::nothrow_t const &) throw();
void operator delete(void *, std::nothrow_t const &) throw();
//
// array new and delete
//
void *operator new[](std::size_t, std::nothrow_t const &) throw();
void operator delete[](void *, std::nothrow_t const &) throw();
這幾個函數(shù)也被認(rèn)為是new 操作和delete 操作的placement 形式,因為它們也接收額
外參數(shù)。和前面的控制對象分配位置的版本不同,這幾個只是讓你分辨出拋異常的new 和不
拋異常的new。
#include <iostream>
#include <new>
using namespace std;
int main()
{
int *p;
//
// 'new' that can throw
//
try
{
p = new int;
}
catch(bad_alloc &)
{
cout << "'new' threw an exception";
}
//
// 'new' that can't throw
//
try
{
p = new(nothrow) int;
}
catch(bad_alloc &)
{
cout << "this line should never appear";
}
//
return 0;
}
注意兩個new 表達(dá)式的重要不同之處:
p = new int;
在分配失敗時拋std::bad_alloc,而
p = new(nothrow) int;
在分配失敗時不拋異常,它返回NULL(就象malloc 和C++黑暗年代的new)。
如果你不喜歡nothrow 的語法,或你的編譯器不支持,你可以這樣達(dá)到同樣效果:
#include <new>
//
// function template emulating 'new(std::nothrow)'
//
template<typename T>
T *new_nothrow() throw()
{
T *p;
try
{
p = new T;
}
catch(std::bad_alloc &)
{
p = NULL;
}
return p;
}
//
// example usage
//
int main()
{
int *p = new_nothrow<int>(); // equivalent to 'new(nothrow) int'
return 0;
}
這個模板函數(shù)與它效仿的new(nothrow)表達(dá)式同有一個潛在的異常安全漏洞。現(xiàn)在,
我將它作為習(xí)題留給你去找出來。(恐怕沒什么用的提示:和placement delete 有關(guān)。)
5.8 小結(jié)
new 和delete 是怪獸。和typeid 一起,它們是C++中僅有的會調(diào)用標(biāo)準(zhǔn)運行庫中函數(shù)
的關(guān)鍵字。即使程序除了main 外不明確調(diào)用或定義任何函數(shù),new 和delete 語句的出現(xiàn)就
會使程序調(diào)用運行庫。如我在這兒所示范的,調(diào)用運行庫將經(jīng)??赡軖伄惓;蛱幚懋惓?。
本篇的例程中的代碼和注釋是用于我對C++標(biāo)準(zhǔn)的解釋的。不幸的是,如我以前所說,
Microsoft 的Visual C++經(jīng)常不遵守C++標(biāo)準(zhǔn)。在下一篇中,我將揭示Visual C++的運行庫
對new 和delete 的支持在什么地方背離了C++標(biāo)準(zhǔn)。我將特別注意在對異常的支持上的背
離,并且將展示怎么繞過它們。
回到目錄
6. Microsoft 對于<new>的實現(xiàn)版本中的異常處理
上次,我講述了標(biāo)準(zhǔn)運行庫頭文件<new>中申明的12 個全局函數(shù)中的異常行為。這次我
將開始討論Microsoft 對這些函數(shù)的實現(xiàn)版本。
在Visual C++ 5 中,標(biāo)準(zhǔn)運行庫頭文件<new>提供了這些申明:
namespace std
{
class bad_alloc;
struct nothrow_t;
extern nothrow_t const nothrow;
};
void *operator new(size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
void *operator new(size_t, void *);
void *operator new(size_t, std::nothrow_t const &) throw();
和在第五部分中講述的標(biāo)準(zhǔn)所要求的相比,Microsoft 的<new>頭文件版本缺少:
?? 所有(三種)形式的operator new[]
?? 所有(三種)形式的operator delete[]
?? Placement operator delete(void *, void *)
?? Placement operator delete(void *, std::nothrow_t const &)
并且,雖然運行庫申明了operator new 拋出std::bad_alloc,但函數(shù)的行為并不符合
標(biāo)準(zhǔn)。
如果你使用Visaul C++ 6,<new>頭文件有同樣的缺陷,只是它申明了operator
delete(void *, void *)。
6.1 數(shù)組
Visual C++在標(biāo)準(zhǔn)運行庫的實行中沒有定義operator new[]和operator delete[]形式
的版本。幸好,你可以構(gòu)建自己的版本:
#include <stdio.h>
void *operator new(size_t)
{
printf("operator new ");
return 0;
}
void operator delete(void *)
{
printf("operator delete ");
}
void *operator new[](size_t)
{
printf("operator new[] ");
return 0;
}
void operator delete[](void *)
{
printf("operator delete[] ");
}
int main()
{
int *p;
p = new int;
delete p;
p = new int[10];
delete[] p;
}
/* When run should yield
operator new
operator delete
operator new[]
operator delete[]
*/
為什么Visual C++的標(biāo)準(zhǔn)運行庫缺少這些函數(shù)?我不能肯定,猜想是“向后兼容”吧。
operator new[]和operator delete[]加入C++標(biāo)準(zhǔn)比較晚,并且許多年來編譯器們還
不支持它,所有支持分配用戶自定義對象的編譯器都定義了operator new 和operator
delete,并且即使是分配數(shù)組對象也將調(diào)用它們。
如果一個以前不支持operator new[]和operator delete[]的編譯器開始支持它們時,
用戶自定義的全局operator new 和operator delete 函數(shù)將不再在分配數(shù)組對象時被調(diào)用。
程序仍然能編譯和運行,但行為卻變了。程序員甚至沒法知道變了什么,因為編譯器沒有報
任何錯。
6.2 無聲的變化
這些無聲的變化給寫編譯器的人(如Microsoft)出了個難題。要知道,C++標(biāo)準(zhǔn)發(fā)展
了近10 年。在此期間,編譯器的賣主跟蹤標(biāo)準(zhǔn)的變化以確保和最終版本的最大程度兼容。
同時,用戶依賴于當(dāng)前可用的語言特性,即使不能確保它們在標(biāo)準(zhǔn)化的過程中得以幸存。
如果標(biāo)準(zhǔn)的一個明顯變化造成了符合前標(biāo)準(zhǔn)的程序的行為的悄然變化,編譯器的賣主有
三種選擇:
1. 堅持舊行為,不理符合新標(biāo)準(zhǔn)的代碼
2. 改到新行為,不理符合舊標(biāo)準(zhǔn)的代碼
3. 讓用戶指定他們想要的行為
在此處的標(biāo)準(zhǔn)運行庫提供operator new[]和operator delete[]的問題上,Micrsoft
選擇了1。我自己希望他們選擇3,對這個問題和其它所有Visual C++不符合標(biāo)準(zhǔn)之處。他
們可以通過#pragmas、編譯選項或環(huán)境變量來判斷用戶的決定的。
Visual C++長期以來通過形如/Za 的編譯開關(guān)來實行選擇3,但這個開關(guān)有一個未公開
的行為:它關(guān)掉了一些標(biāo)準(zhǔn)兼容的特性,然后打開了另外一些。我期望的(想來也是大部分
人期望的)是一個完美的調(diào)節(jié)方法來打開和關(guān)閉標(biāo)準(zhǔn)兼容的特性!
(在這個operator new[]和operator delete[]的特例中,我建議你開始使用容器類(如
vector)來代替數(shù)組,但這是另外一個專欄的事情了。 )
6.3 異常規(guī)格申明
Microsoft 的<new>頭文件正確地申明了非placement 的operator new:
void *operator new(std::size_t) throw(std::bad_alloc);
你可以定義自己的operator new 版本來覆蓋運行庫的版本,你可能寫成:
void *operator new(std::size_t size) throw(std::bad_alloc)
{
void *p = NULL;
// ... try to allocate '*p' ...
if (p == NULL)
throw std::bad_alloc();
return p;
}
如果你保存上面的函數(shù),并用默認(rèn)選項編譯,Visual C++不會報錯。但,如果你將警告
級別設(shè)為4,然后編譯,你將遇到這個信息:
warning C4290: C++ Exception Specification ignored
那么好,如果你自己的異常規(guī)格申明不能工作,肯定,運行庫的版本也不能。保持警告
級別為4,然后編譯:
#include <new>
我們已經(jīng)知道,它申明了一個和我們的程序同樣的異常規(guī)格的函數(shù)。
奇怪啊,奇怪!編譯器沒有警告,即使在級別4!這是否意味著運行庫的申明有些奇特
屬性而我們的沒有?不,它事實上意味著Micorsoft 的欺騙行為:
?? <new>包含了標(biāo)準(zhǔn)運行庫頭文件<exception>。
?? <exception>包含了非標(biāo)頭文件xstddef。
?? xstddef 包含了另一個非標(biāo)頭文件yvals.h。
?? yvals.h 包含了指令#pragma warning(disable:4290)。
?? #pragma 關(guān)閉了特定的級別4 的警告,我們在自己的代碼中看到的那條。
結(jié)論:Visual C++在編譯期檢查異常規(guī)格申明,但在運行期忽略它們。你可以給函數(shù)加
上異常申明(如throw(std::bad_alloc)),編譯器會正確地分析它們,但在運行期這個申
明沒有效果,就象根本沒有寫過。
6.4 怎么會這樣
在這個專欄的第三部分,我講述了異常規(guī)格申明的形式,卻沒有解釋其行為和效果。
Visual C++對異常規(guī)格申明的不完全支持給了我一個極好的機會來解釋它們。
異常規(guī)格申明是函數(shù)及其調(diào)用者間契約的一部分。它完整列舉了函數(shù)可能拋出的所有異
常。(用標(biāo)準(zhǔn)中的說法,被稱為函數(shù) “允許”特定的異常。)
換句話說就是,函數(shù)不允許(承諾不拋出)其它任何不在申明中的異常。如果申明有但
為空,函數(shù)根本不允許任何異常;相反,如果沒有異常規(guī)格申明,函數(shù)允許任何異常。
除非函數(shù)與調(diào)用者間的契約是強制性的,否則它根本就不值得寫出來。于是你可能會想,
編譯器應(yīng)該在編譯時確保函數(shù)沒有撒謊:
void f() throw() // 'f' promises to throw no exceptions...
{
throw 1; // ... yet it throws one anyway!
}
驚訝的是,它在Visual C++中編譯通過了。
不要認(rèn)為Visual c++有病,這個例子可以用任何兼容C++的編譯器編譯通過。我從標(biāo)準(zhǔn)
(sub clause 15.4p10)中引下來的:
C++的實現(xiàn)版本不該拒絕一個表達(dá)式,僅僅是因為它拋出或可能拋出一個其相關(guān)函數(shù)所
不允許的異常。例如:
extern void f() throw(X, Y);
void g() throw(X)
{
f(); //OK
}
調(diào)用f()的語句被正常編譯,即使當(dāng)調(diào)用時f()可能拋出g()不允許的異常Y。
是不是有些特別?那么好,如果編譯器不強制這個契約,將發(fā)生什么?
6.5 運行期系統(tǒng)
如果函數(shù)拋出了一個它承諾不拋的異常, 運行期系統(tǒng)調(diào)用標(biāo)準(zhǔn)運行庫函數(shù)
unexpected()。運行庫的缺省unexpected()的實現(xiàn)是調(diào)用terminate()來結(jié)束函數(shù)。你可以
調(diào)用set_unexpected()函數(shù)安裝新的unexpected()處理函數(shù)而覆蓋其缺省行為。
這只是理論。但如前面的Visual C++警告所暗示,它忽略了異常規(guī)格申明。因此,Visual
C++運行期系統(tǒng)不會調(diào)用unexpected()函數(shù),當(dāng)一個函數(shù)違背其承諾時。
要試一下你所喜愛的編譯器的行為,編譯并運行下面這個小程序:
#include <exception>
#include <stdio.h>
using namespace std;
void my_unexpected_handler()
{
throw bad_exception();
}
void promise_breaker() throws()
{
throw 1;
}
int main()
{
set_unexpected(my_unexpected_handler);
try
{
promise_breaker();
}
catch(bad_exception &)
{
printf("Busted!");
}
catch(...)
{
printf("Escaped!");
}
return 0;
}
如果程序輸出是:
Busted!
則,運行期系統(tǒng)完全捕獲了違背異常規(guī)格申明的行為。反之,如果輸出是:
Escaped!
則運行期系統(tǒng)沒有捕獲違背異常規(guī)格申明的行為。
在這個程序里,我安裝了my_unexepected_handler()來覆蓋運行庫的缺省unexpected()
處理函數(shù)。這個自定義的處理函數(shù)拋出一個std::bad_exception 類型的異常。此類型有特
別的屬性:如果unexpected()異常處理函數(shù)拋出此類型,此異常能夠被(外面)捕獲,程
序?qū)⒗^續(xù)運行而不被終止。在效果上,這個bad_exception 對象代替了原始的拋出對象,并
向外傳播。
這是假定了編譯器正確地檢測了unexpected 異常, 在Visual C++ 中,
my_unexpected_handler() 沒有并調(diào)用,原始的int 型異常拋到了外面,違背了承諾。
6.6 模擬異常規(guī)格申明
如果你愿意你的設(shè)計有些不雅,就可以在Visual C++下模擬異常規(guī)格申明??紤]一下
這個函數(shù)的行為:
void f() throw(char, int, long)
{
// ... whatever
}
假設(shè)一下會發(fā)生什么?
?? 如果f()沒有發(fā)生異常,它正常返回。
?? 如果f()發(fā)生了一個允許的異常,異常傳到f()外面。
?? 如果f()發(fā)生了其它(不被允許)的異常,運行期系統(tǒng)調(diào)用unexpected()函數(shù)。
要在Visual C++下實現(xiàn)這個行為,要將函數(shù)改為:
void f() throw(char, int, long)
{
try
{
// ... whatever
}
catch(char)
{
throw;
}
catch(int)
{
throw;
}
catch(long)
{
throw;
}
catch(...)
{
unexpected();
}
}
Visual C++一旦開始正確支持異常規(guī)格申明,它的內(nèi)部代碼必然象我在這兒演示的。這
意味著異常規(guī)格申明將和try/catch 塊一樣導(dǎo)致一些代價,就象我在第四部分中演示的。
因此,你應(yīng)該明智地使用異常規(guī)格申明,就象你使用其它異常部件。任何時候你看到一
個異常規(guī)格申明,你應(yīng)該在腦子里將它們轉(zhuǎn)化為try/catch 隊列以正確地理解其相關(guān)的代
價。
6.7 預(yù)告
placement delete 的討論要等到下次。將繼續(xù)討論更多的通行策略來異常保護你的設(shè)
計。
回到目錄
7. 部分構(gòu)造及placement delete
討論在一般情況下的部分構(gòu)造、動態(tài)生成對象時的部分構(gòu)造,以及用 placement delete
來解決部分構(gòu)造問題。
C++標(biāo)準(zhǔn)要求標(biāo)準(zhǔn)運行庫頭文件<new>提供幾個operator delete 的重載形式。在這些重
載形式中,Visual C++ 6 缺少:
?? void operator delete(void *, void *)
而Visual C++ 5 缺少:
?? void operator delete(void *, void *)
?? void operator delete(void *, std::nothrow_t const &)
這些重載形式支持placement delete 表達(dá)式,并解決了一個特殊問題:釋放部分構(gòu)造
的對象。在這次和接下來一次,我將給出一般情況下的部分構(gòu)造、動態(tài)生成對象時的部分構(gòu)
造,以及用 placement delete 來解決部分構(gòu)造問題的例子。
7.1 部分構(gòu)造
看這個例子:
// Example 1
#include <iostream>
class A
{
public:
A()
{
throw 0;
}
};
int main()
{
try
{
A a;
}
catch(...)
{
std::cout <<"caught exception" << std::endl;
}
return 0;
}
因為A 的構(gòu)造函數(shù)拋出了一個異常,a 對象沒有完全構(gòu)造。在這個例子中,沒有構(gòu)造函
數(shù)有可見作用:因為A 沒有子對象,構(gòu)造函數(shù)實際上沒有任何操作。但,考慮這樣的變化:
// Example 2
#include <iostream>
class B
{
public:
B()
{
throw 0;
}
};
class A
{
private:
B const b;
};
// ... main same as before ...
現(xiàn)在,A 的構(gòu)造函數(shù)不是無行為的,因為它構(gòu)造了一個B 成員對象,而它里面會拋異常。
程序?qū)@個異常作出什么反應(yīng)?
從C++標(biāo)準(zhǔn)中摘下了四條(稍作了簡化)原則:
?? 一個對象被完全構(gòu)造,當(dāng)且僅當(dāng)它的構(gòu)造函數(shù)已經(jīng)完全執(zhí)行,而它的析構(gòu)函數(shù)還沒開始
執(zhí)行。
?? 如果一個對象包含子對象,包容對象的構(gòu)造函數(shù)只有在所有子對象被完全構(gòu)造后才開始
執(zhí)行。
?? 一個對象被析構(gòu),當(dāng)且僅當(dāng)它被完全構(gòu)造。
?? 對象按它們被構(gòu)造的反序進行析構(gòu)。
因為拋出了一個異常,B::B 沒有被完全執(zhí)行。因此,B 的對象A::b 既沒有被完全構(gòu)造
也沒有被析構(gòu)。
要證明這點,跟蹤相應(yīng)的類成員:
// Example 3
#include <iostream>
class B
{
public:
B()
{
std::cout << "B::B enter" << std::endl;
throw 0;
std::cout << "B::B exit" << std::endl;
}
~B()
{
std::cout << "B::~B" << std::endl;
}
};
class A
{
public:
A()
{
std::cout << "A::A" << std::endl;
}
~A()
{
std::cout << "A::~A"<< std::endl;
}
private:
B const b;
};
// ... main same as before ...
當(dāng)運行時,程序?qū)⒅惠敵?br>B::B enter
caught exception
從而顯示出對象a 和b 既沒有完全構(gòu)造也沒有析構(gòu)。
7.2 多對象
使例子變得更有趣和更有說明力,把它改得允許部分(不是全部)對象被完全構(gòu)造:
// Example 4
#include <iostream>
class B
{
public:
B(int const ID) : ID_(ID)
{
std::cout << ID_ << " B::B enter" <<std::endl;
if (ID_ > 2)
throw 0;
std::cout << ID_ << " B::B exit" <<std::endl;
}
~B()
{
std::cout << ID_ << " B::~B" <<std::endl;
}
private:
int const ID_;
};
class A
{
public:
A() : b1(1), b2(2), b3(3)
{
std::cout <<"A::A" << std::endl;
}
~A()
{
std::cout <<"A::~A" << std::endl;
}
private:
B const b1;
B const b2;
B const b3;
};
// ... main same asbefore ...
注意B 的構(gòu)造函數(shù)現(xiàn)在接受一個對象ID 值的參數(shù)。用它作B 的對象的唯一標(biāo)記并決定
對象是否完全構(gòu)造。大部分跟蹤信息以這些ID 開頭,顯示為:
1 B::B enter
1 B::B exit
2 B::B enter
2 B::B exit
3 B::B enter
2 B::~B
1 B::~B
caught exception
b1 和b2 完全構(gòu)造而b3 沒有。所以,b1 和b2 被析構(gòu)而b3 沒有。此外,b1 和b2 的析
構(gòu)按其構(gòu)造的反序進行。最后,因為一個子對象(b3)沒有完全構(gòu)造,包容對象a 也沒有完
全構(gòu)造和析構(gòu)。
7.3 動態(tài)分配對象
將類A 改為其成員變量是動態(tài)生成的:
// Example 5
#include <iostream>
// ... class B same as before ...
class A
{
public:
A() : b1(new B(1)), b2(new B(2)), b3(new B(3))
{
std::cout <<"A::A" << std::endl;
}
~A()
{
delete b1;
delete b2;
delete b3;
std::cout <<"A::~A" << std::endl;
}
private:
B * const b1;
B * const b2;
B * const b3;
};
// ... main same as before ...
這個形式符合C++習(xí)慣用法:在包容對象的構(gòu)造函數(shù)里分配成員變量,并對其填充數(shù)據(jù),
然后在包容對象的析構(gòu)函數(shù)里釋放它們。
編譯并運行例5。輸出是:
1 B::B enter
1 B::B exit
2 B::B enter
2 B::B exit
3 B::B enter
caught exception
其結(jié)果與例4 相似,但有一個巨大的不同:因為~A 沒有被執(zhí)行,其中的delete 語句也
就沒有執(zhí)行,被成功分配的*b1 和*b2 的析構(gòu)函數(shù)也沒有調(diào)用。例四中的不妙狀況(三個對
象析構(gòu)了兩個)現(xiàn)在更差了(三個對象一個都沒有析構(gòu))。
實際上,沒有比這更壞的了。記住,delete b1 語句有兩個作用:
?? 調(diào)用*b1 的析構(gòu)函數(shù)~b。
?? 調(diào)用operator delete 釋放*b1 所占有的內(nèi)存。
所以我們不光是遇到~B 沒有被調(diào)用所導(dǎo)致的問題,還有每個B 對象造成的內(nèi)存泄漏問
題。這不是件好事。
B 對象是A 私有的,它們是實現(xiàn)細(xì)節(jié),對程序的其它部分是不可見的。用動態(tài)生成B 的
子對象來代替自動生成B 的子對象不該改變程序的外在行為,這表明了我們的例子在設(shè)計上
的缺陷。
7.4 析構(gòu)動態(tài)生成的對象
為了最接近例4 的行為,我們需要在任何情況強迫delete 語句的執(zhí)行。將它們放入~A
明顯不起作用。我們需要找個能起作用的地方,我們知道它能被執(zhí)行的地方。跳入腦海的解
決方法中,最優(yōu)雅的方法來自于C++標(biāo)準(zhǔn)運行庫:
// Example 6
#include <iostream>
#include <memory>
// ... class B same as before ...
class A
{
public:
A() : b1(new B(1)), b2(new B(2)), b3(new B(3))
{
std::cout << "A::A" << std::endl;
}
~A()
{
std::cout << "A::~A" << std::endl;
}
private:
std::auto_ptr<B> const b1;
std::auto_ptr<B> const b2;
std::auto_ptr<B> const b3;
};
// ... main same as before ...
auot_ptr 讀作“auto-pointer”。如名所示,auoto-pointer 表現(xiàn)為通常的指針和自動
對象的混合體。
std::auto_ptr 是在<memory>中申明的類模板。一個std::auto_ptr<B>類型的對象的表
現(xiàn)非常象一個通常的B*類型對象,關(guān)鍵的不同是:auto_ptr 是一個實實在在的類對象,它
有析構(gòu)函數(shù),而這個析構(gòu)函數(shù)將在B*所指對象上調(diào)用delete。最終結(jié)果是:動態(tài)生成的B
對象如同是個自動B 對象一樣被析構(gòu)。
可以把一個auto_ptr<B>對象當(dāng)作對動態(tài)生成的B 對象的簡單包裝。在包裝消失(析構(gòu))
時,它也將被包裝對象帶走了。要實際看這個魔術(shù)戲法,編譯并運行例6。結(jié)果是:
1 B::B enter
1 B::B exit
2 B::B enter
2 B::B exit
3 B::B enter
2 B::~B
1 B::~B
caught exception
Bingo!輸出和例4 相同。
你可能會奇怪為什么沒有為b3 調(diào)用~B。這表明了auto_ptr 包裝上的失?。扛静皇?。
我們所讀過的規(guī)則還在起作用。對b3 進行的構(gòu)造函數(shù)的調(diào)用接受了new B(3)傳過來的參數(shù)。
于是發(fā)生了一個異常終止了b3 的構(gòu)造。因為b3 沒有完全構(gòu)造,它同樣不會析構(gòu)。
藏在atuo-pointer 后面的想法沒有新的地方;string 對象實際上就是char 數(shù)組的
auto-pointer 型包裝。雖然如此,我仍然期望有一天我能更詳細(xì)的討論auto_ptr 及其家族,
目前只要把auto_ptr 當(dāng)作一個保證發(fā)生異常時能析構(gòu)動態(tài)生成的對象的簡單方法。
7.5 預(yù)告
既然b3 的析構(gòu)函數(shù)沒有被調(diào)用,也就沒有為其內(nèi)存調(diào)用delete。如前面所見,被包裝
的B 對象受到兩個影響:
?? 析構(gòu)函數(shù)~B 沒有被調(diào)用。這是意料中的甚至是期望中的,因為B 對象在先前沒有完全
構(gòu)造。
?? 內(nèi)存沒有被通過operator delete 釋放。不管是不是意料中的,它絕不是期望中的,因
為B 對象所占用的內(nèi)存被分配了,即使B 對象沒有在此內(nèi)存中完全構(gòu)造。
我需要operator delete 被調(diào)用,即使~B 沒有被調(diào)用。要實現(xiàn)這點,編譯器必須在脫
離delete 語句的情況下調(diào)用operator delete。因為我知道b3 是我的例子中的討厭對象,
我可以顯式地為b3 的內(nèi)存調(diào)用operator delete;但要知道這只是教學(xué)程序,通常情況下
我們不能預(yù)知哪個構(gòu)造函數(shù)將失敗。
不,我們所需要的是編譯器檢測到動態(tài)生成對象時的構(gòu)造函數(shù)失敗時隱含調(diào)用
operator delete 來釋放對象占用的內(nèi)存。這有些效仿編譯器在自動對象構(gòu)造失敗時的行為:
對象的內(nèi)存如同程序體中的無用單元一樣,是可回收的。
幸好,它有個大喜結(jié)局。要看這個結(jié)局,需到下回。在下回結(jié)束時,我將揭示C++語言
如何提供了這個完美特性,為什么標(biāo)準(zhǔn)運行庫申明了placement operator delete,以及為
什么你可能想在自己的庫或類中做同樣的事。
回到目錄
8. 自動刪除,類屬new 和delete、placement new 和placement delete
在上次結(jié)束時,我期望道:當(dāng)一個新產(chǎn)生的對象在沒有完全構(gòu)造時,它所占用的內(nèi)存能
自動釋放。很幸運,C++標(biāo)準(zhǔn)委員會將這個功能加入到了語言中(而不幸的是,這個特性加
得太晚了,許多編譯器還不支持它)。Visual C++ 5 和6 都支持這個“自動刪除”特性(但,
如我們將要看到的,Visual C++ 5 的支持是不完全的)。
8.1 自動刪除
要實際驗證它,在上次的例6 中增加帶跟蹤信息的operator new 和operator delete
函數(shù):
// Example 7
#include <iostream>
#include <memory>
#include <stdio.h>
#include <stdlib.h>
void *operator new(size_t const n)
{
printf(" ::operator new ");
return malloc(n);
}
void operator delete(void *const p)
{
std::cout << " ::operator delete" << std::endl;
free(p);
}
class B
{
public:
B(int const ID) : ID_(ID)
{
std::cout << ID_ << " B::B enter" << std::endl;
if (ID_ > 2)
{
std::cout << std::endl;
std::cout << " THROW" << std::endl;
std::cout << std::endl;
throw 0;
}
std::cout << ID_ << " B::B exit" << std::endl;
}
~B()
{
std::cout << ID_ << " B::~B" << std::endl;
}
private:
int const ID_;
};
class A
{
public:
A() : b1(new B(1)), b2(new B(2)), b3(new B(3))
{
std::cout << " A::A" << std::endl;
}
~A()
{
std::cout << " A::~A" << std::endl;
}
private:
std::auto_ptr<B> const b1;
std::auto_ptr<B> const b2;
std::auto_ptr<B> const b3;
};
int main()
{
try
{
A a;
}
catch(...)
{
std::cout << std::endl;
std::cout << " CATCH" << std::endl;
std::cout << std::endl;
}
return 0;
}
程序?qū)⒂梦覀冏约旱膐perator new 和operator delete 代替標(biāo)準(zhǔn)運行庫提供的版本。
這樣,我們將能跟蹤所有的動態(tài)創(chuàng)建對象時的分配和釋放內(nèi)存操作。(我同時小小修改了其
它的跟蹤信息,以便輸出信息更容易讀。)
注意,我們的operator new 調(diào)用了printf 而不是std::cout。本來,我確實使用了
std::cout,但程序在運行庫中產(chǎn)生了一個無效頁錯誤。調(diào)試器顯示運行庫在初始化
std::cout 前調(diào)用了operator new,而operator new 又試圖調(diào)用還沒有初始化的std::cout,
程序于是崩潰了。
我在Visual C++ 6 中運行程序,得到了頭大的輸出:
::operator new
::operator new
::operator new
::operator new
::operator new
::operator new
::operator delete
::operator delete
::operator new
::operator new
::operator new
::operator new
::operator new
::operator new
::operator delete
::operator delete
1 B::B enter
1 B::B exit
::operator new
2 B::B enter
2 B::B exit
::operator new
3 B::B enter
THROW
::operator delete
2 B::~B
::operator delete
1 B::~B
::operator delete
CATCH
::operator delete
::operator delete
::operator delete
::operator delete
::operator delete
Blech.
我無法從中分辨出有用的信息。原因很簡單:我們的代碼,標(biāo)準(zhǔn)運行庫的代碼,以及編
譯器暗中生成的代碼都調(diào)用了operator new 和operator delete。我們需要一些方法來隔
離出我們感興趣的調(diào)用過程,并只輸出它們的跟蹤信息。
8.2 類屬new 和delete
C++又救了我們。不用跟蹤全局的operator new 和operator delete,我們可以跟蹤其
類屬版本。既然我們感興趣的是B 對象的分配和釋放過程,我們只需將operator new 和
operator delete 移到類B 中去:
// Example 8
#include <iostream>
#include <memory>
class B
{
public:
void *operator new(size_t const n)
{
std::cout << " B::operator new" << std::endl;
return ::operator new(n);
}
void operator delete(void *const p)
{
std::cout << " B::operator delete" << std::endl;
operator delete(p);
}
// ... rest of class B unchanged
};
// ... class A and main unchanged
編譯器將為B 的對象調(diào)用這些函數(shù),而為其它對象的分配和釋放調(diào)用標(biāo)準(zhǔn)運行庫中的函
數(shù)版本。
通過在你自己的類這增加這樣的局部操作函數(shù),你可以更好的管理動態(tài)創(chuàng)建的此類型對
象。例如,嵌入式系統(tǒng)的程序員經(jīng)常在特殊映射的設(shè)備或快速內(nèi)存中分配某些對象,通過其
類型特有的operator new 和operator delete,可以控制如何及在哪兒分配這些對象。
對我們的例子,特殊的堆管理是沒必要的。因此,我在類屬operator new 和operator
delete 中調(diào)用了其全局版本而不再是malloc 和free,并去除了對頭文件<stdlib.h>的包含。
這樣,所有對象的分配和釋放的實際語義保持了一致。
同時,因為我們的operator new 不在在全局范圍內(nèi),它不會被運行庫在構(gòu)造std::cout
前調(diào)用,于是我可以在其中安全地調(diào)用std::cout 了。因為不再調(diào)用printf,我也去掉了
<stdio.h>。
編譯并運行例8。將發(fā)現(xiàn)輸出信息有用多了:
B::operator new
1 B::B enter
1 B::B exit
B::operator new
2 B::B enter
2 B::B exit
B::operator new
3 B::B enter
THROW
B::operator delete
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
三個B::operator new 的跟蹤信息對應(yīng)于a.b1、a.b2 和a.b3 的構(gòu)造。其中,a.b1 和
a.b2 被完全構(gòu)造(它們的構(gòu)造函數(shù)都進入并退出了),而a.b3 沒有(它的構(gòu)造函數(shù)只是進
入了而沒有退出)。注意這個:
3 B::B enter
THROW
B::operator delete
它表明,調(diào)用a.b3 的構(gòu)造函數(shù),在其中拋出了異常,然后編譯器自動釋放了a.b3 占用的內(nèi)
存。接下來的跟蹤信息:
2 B::~B
B::operator delete
1 B::~B
B::operator delete
表明被完全構(gòu)造的對象a.b2 和a.b1 在釋放其內(nèi)存前先被析構(gòu)了。
結(jié)論:所有完全構(gòu)造的對象的析構(gòu)函數(shù)被調(diào)用,所有對象的內(nèi)存被釋放。
8.3 Placement new
例8 使用了“普通的”非Placement new 語句來構(gòu)造三個B 對象。現(xiàn)在考慮這個變化:
// Example 9
// ... preamble unchanged
class B
{
public:
void *operator new(size_t const n, int)
{
std::cout << " B::operator new(int)" << std::endl;
return ::operator new(n);
}
// ... rest of class B unchanged
};
class A
{
public:
A() : b1(new(0) B(1)), b2(new(0) B(2)), b3(new(0) B(3)) {
std::cout << " A::A" << std::endl;
}
// ... rest of class A unchanged
};
// ... main unchanged
這個new 語句
new(0) B(1)
有一個placement 參數(shù)0。因為參數(shù)的類型是int,編譯器需要operator new 的一個接受額
外int 參數(shù)的重載版本。我已經(jīng)增加了一個滿足要求的B::operator new 函數(shù)。這個函數(shù)實
際上并不使用這個額外參數(shù),此參數(shù)只是個占位符,用來區(qū)分 placement new 還是非
placement new 的。
因為Visual C++ 5 不完全支持 placement new 和 placement delete,例9 不能在其
下編譯。程序在Visual C++ 6 下能編譯,但在下面這行上生成了三個Level 4 的警告:
A() : b1(new(0) B(1)), b2(new(0) B(2)), b3(new(0) B(3))
內(nèi)容都是:
'void *B::operator new(unsigned int, int)':
no matching operator delete found;
memory will not be freed if initialization
throws an exception
想知道編譯器為什么警告,運行程序,然后和例8 比較輸出:
B::operator new(int)
1 B::B enter
1 B::B exit
B::operator new(int)
2 B::B enter
2 B::B exit
B::operator new(int)
3 B::B enter
THROW
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
輸出是相同的,只一個關(guān)鍵不同:
3 B::B enter
THROW
和例8 一樣的是,a.b3 的構(gòu)造函數(shù)進入了并在其中拋出了異常;但和例8 不同的是,a.b3
的內(nèi)存沒有自動刪除。我們應(yīng)該留意編譯器的警告的!
8.4 最后,Placement delete!
想要“自動刪除”能工作,一個匹配拋異常的對象的operator new 的operator delete
的重載版本必須可用。摘自 C++標(biāo)準(zhǔn) (subclause 5.3.4p19, "New"):
如果參數(shù)的數(shù)目相同并且除了第一個參數(shù)外其類型一致(在作了參數(shù)的自動類型轉(zhuǎn)換
后),一個placement 的釋放函數(shù)與一個placement 的分配函數(shù)相匹配。所有的非palcement
的釋放函數(shù)匹配于一個非placement 的分配函數(shù)。如果找且只找到一個匹配的釋放函數(shù),這
個函數(shù)將被調(diào)用;否則,沒有釋放函數(shù)被調(diào)用。
因此,對每個placement 分配函數(shù)
void operator new(size_t, P2, P3, ..., Pn);
都有一個對應(yīng)的placement 釋放函數(shù)
void *operator delete(void *, P2, P3, ..., Pn);
這里
P2, P3, ..., Pn
一般是相同的參數(shù)隊列。我說“一般”是因為,根據(jù)標(biāo)準(zhǔn)的說法,可以對參數(shù)進行一些轉(zhuǎn)換。
再引于標(biāo)準(zhǔn)(subclause 8.3.5p3, "Functions"),基于可讀性稍作了修改:
在提供了參數(shù)類型列表后,將對這些類型作一些轉(zhuǎn)換以決定函數(shù)的類型:
?? 所有參數(shù)類型的const/volatile 描述符修飾將被刪除。這些cv 描述符修飾只影響形
參在函數(shù)體中的定義,不影響函數(shù)本身的類型。
例如:類型
void (*)(const int)
變?yōu)?br>void (*)(int)
?? 如果一個存儲類型描述符修飾了一個參數(shù)類型,此描述符被刪除。這存儲類型描述符
修飾只影響形參在函數(shù)體中的定義,不影響函數(shù)本身的類型。
例如:
register char *
變成
char *
轉(zhuǎn)換后的參數(shù)類型列表才是函數(shù)的參數(shù)類型列表。
順便提一下,這個規(guī)則同樣影響函數(shù)的重載判斷,signatures 和name mangling?;?br>上,函數(shù)參數(shù)上的cv 描述符和存儲類型描述符的出現(xiàn)不影響函數(shù)的身份。例如,這意味著
下列所有申明引用的是同一個函數(shù)的定義。
?? void f(int)
?? void f(const int)
?? void f(register int)
?? void f(auto const volatile int)
增加匹配于我們的placement operator new 的placement operator delete 函數(shù):
// Example 10
// ... preamble unchanged
class B
{
public:
void operator delete(void *const p, int)
{
std::cout << " B::operator delete(int)" << std::endl;
::operator delete(p);
}
// ... rest of class B unchanged
};
// ... class A and main unchanged
然后重新編譯并運行。輸出是:
B::operator new(int)
1 B::B enter
1 B::B exit
B::operator new(int)
2 B::B enter
2 B::B exit
B::operator new(int)
3 B::B enter
THROW
B::operator delete(int)
2 B::~B
B::operator delete
1 B::~B
B::operator delete
CATCH
和例8 非常相似,每個operator new 匹配一個operator delete。
一個可能奇怪的地方:所有B 對象通過placement operator new 分配,但不是全部通
過placement operator delete 釋放。記住,placement operator delete 只(在plcaement
operator new 失敗時)被調(diào)用于自動摧毀部分構(gòu)造的對象。完全構(gòu)造的對象將通過delete
語句手工摧毀,而delete 語句調(diào)用非placement operator delete。(WQ 注:沒有辦法調(diào)用
placement delete 語句,只能調(diào)用plcaement operator delete 函數(shù),見9.2。)
8.5 光陰似箭
在第九部分,我將展示placement delete 是多么地靈巧(遠(yuǎn)超過現(xiàn)在展示的),但有小
小的隱瞞和簡化。并示范一個新的機制來在構(gòu)造函數(shù)(如A::A)中更好地容忍異常。
回到目錄
9. placement new 和placement delete,及處理構(gòu)造函數(shù)拋出的異常
當(dāng)被調(diào)用了來清理部分構(gòu)造時,operator delete 的第一個void *參數(shù)帶的是對象的地
址(剛剛由對應(yīng)的operator new 返回的)。operator delete 的所有額外placement 參數(shù)都
和傳給operator new 的相應(yīng)參數(shù)的值相匹配。
在代碼里,語句
p = new(n1, n2, n3) T(c1, c2, c3);
的效果是
p = operator new(sizeof(T), n1, n2, n3);
T(p, c1, c2, c3);
如果T(p, c1, c2, c3)構(gòu)造函數(shù)拋出了一個異常,程序暗中調(diào)用
operator delete(p, n1, n2, n3);
原則:當(dāng)釋放一個部分構(gòu)造的對象時,operator delete 從原始的new 語句知道上下文。
9.1 Placement operator delete 的參數(shù)
要證明這點,增強我們的例子來跟蹤相應(yīng)的參數(shù)值:
// Example 11
#include <iostream>
#include <memory>
class B
{
public:
B(int const ID) : ID_(ID)
{
std::cout << ID_ << " B::B enter" << std::endl;
if (ID_ > 2)
{
std::cout << std::endl;
std::cout << " THROW" << std::endl;
std::cout << std::endl;
throw 0;
}
std::cout << ID_ << " B::B exit" << std::endl;
}
~B()
{
std::cout << ID_ << " B::~B" << std::endl;
}
//
// non-placement
//
void *operator new(size_t const n)
{
void *const p = ::operator new(n);
std::cout << " B::operator new(" << n <<
") => " << p << std::endl;
return p;
}
void operator delete(void *const p)
{
std::cout << " B::operator delete(" << p <<
")" << std::endl;
::operator delete(p);
}
//
// placement
//
void *operator new(size_t const n, int const i)
{
void *const p = ::operator new(n);
std::cout << " B::operator new(" << n <<
", " << i << ") => " << p << std::endl;
return p;
}
void operator delete(void *const p, int const i)
{
std::cout << " B::operator delete(" << p <<
", " << i << ")" << std::endl;
::operator delete(p);
}
private:
int const ID_;
};
class A
{
public:
A() : b1(new(11) B(1)), b2(new(22) B(2)), b3(new(33) B(3))
{
std::cout << " A::A" << std::endl;
}
~A()
{
std::cout << " A::~A" << std::endl;
}
private:
std::auto_ptr<B> const b1;
std::auto_ptr<B> const b2;
std::auto_ptr<B> const b3;
};
int main()
{
try
{
A a;
}
catch(...)
{
std::cout << std::endl;
std::cout << " CATCH" << std::endl;
std::cout << std::endl;
}
return 0;
}
用Visual C++ 6 編譯并運行。在我的機器上的輸出是:
B::operator new(4, 11) => 007E0490
1 B::B enter
1 B::B exit
B::operator new(4, 22) => 007E0030
2 B::B enter
2 B::B exit
B::operator new(4, 33) => 007E0220
3 B::B enter
THROW
B::operator delete(007E0220, 33)
2 B::~B
B::operator delete(007E0030)
1 B::~B
B::operator delete(007E0490)
CATCH
注意這些數(shù)字:
?? 4 是每個被分配的B 對象的大小的字節(jié)數(shù)。這個值在不同的C++實現(xiàn)下差異很大。
?? 如007E0490 這樣的值是operator new 返回的對象的地址,作為this 指針傳給T 的成
員函數(shù)的,并作為void *型指針傳給operator delete。你看到的值幾乎肯定和我的不
一樣。
?? 11,22 和33 是最初傳給operator new 的額外placement 參數(shù),并在部分構(gòu)造時傳給
相應(yīng)的placement operator delete。
9.2 手工調(diào)用operator delete
所有這些operator new 和operator delete 的自動匹配是很方便的,但它只在部分構(gòu)
造時發(fā)生。對通常的完全構(gòu)造,operator delete 不是被自動調(diào)用的,而是通過明確的delete
語句間接調(diào)用的:
p = new(1) B(2); // calls operator new(size_t, int)
// ...
delete p; // calls operator delete(void *)
這樣的順序其結(jié)果是調(diào)用placement operator new 和非placement operator delete,
即使你有對應(yīng)的(placement)operator delete 可用。
雖然你很期望,但你不能用這個方法強迫編譯器調(diào)用placement operator delete:
delete(1) p; // error
而必須手工寫下delete 語句將要做的事:
p->~B(); // call *p's destructor
B::operator delete(p, 1); // call placement
// operator delete(void *, int)
要和自動調(diào)用operator delete 時的行為保持完全一致,你必須保存通過new 語句傳給
operator new 的參數(shù),并將它們手工傳給operator delete。
p = new(n1, n2, n3) B;
// ...
p->~B();
B::operator delete(p, n1, n2, n3);
9.3 其它非placement delete
貫穿整個這個專題,我說了operator new 和operator delete 分類如下:
函數(shù)對
?? void *operator new(size_t)
?? void operator delete(void *)
是非placement 分配和釋放函數(shù)。
所有如下形式的函數(shù)對
?? void *operator new(size_t, P1, ..., Pn)
?? void operator delete(void *, P1, ..., Pn)
是placement 分配和釋放函數(shù)。
我這樣說是因為簡潔,但我現(xiàn)在必須承認(rèn)撒了個小謊:
void operator delete(void *, size_t)
也可以是一個非placement 釋放函數(shù)而匹配于
void *operator new(size_t)
雖然它有一個額外參數(shù)。如你所猜想,operator delete 的size_t 參數(shù)帶的是傳給operator
new 的size_t 的值。和其它額外參數(shù)不同,它是提供完全構(gòu)造的對象用的。
在我們的例子中,將這個size_t 參數(shù)加到非placement operator delete 上:
// Example 12
// ... preamble unchanged
class B
{
void operator delete(void * const p, size_t const n)
{
std::cout << " B::operator delete(" << p <<
", " << n << ")" << std::endl;
::operator delete(p);
}
// ... rest of class B unchanged
};
// ... class A and main unchanged
The results:
B::operator new(4, 11) => 007E0490
1 B::B enter
1 B::B exit
B::operator new(4, 22) => 007E0030
2 B::B enter
2 B::B exit
B::operator new(4, 33) => 007E0220
3 B::B enter
THROW
B::operator delete(007E0220, 33)
2 B::~B
B::operator delete(007E0030, 4)
1 B::~B
B::operator delete(007E0490, 4)
CATCH
注意,為完全構(gòu)造的對象,將額外的參數(shù)4 提供給了operator delete。
9.4 顯而易見的矛盾
你可能奇怪:C++標(biāo)準(zhǔn)允許非placement operator delete 自動知道一個對象的大小,
卻否定了placement operator delete 可具有相同的能力。要想使它們保持一致,一個
placement 分配函數(shù)
void *operator new(size_t, P1, P2, P3)
應(yīng)該匹配于這樣一個placement 釋放函數(shù)
void operator delete(void *, size_t, P1, P2, P3)
但事實不是這樣,這兩個函數(shù)不匹配。為什么語言被這樣設(shè)計?我猜有兩個原因:效率
和清晰。
大部分情況下,operator delete 不需要知道一個對象的大??;強迫函數(shù)任何時候都接
受大小參數(shù)是低效的。并且,如果標(biāo)準(zhǔn)允許size_t 參數(shù)可選,這樣的含糊將造成:
void operator delete(void *, size_t, int)
在不同的環(huán)境下有不同的意義,決定它將匹配哪個:
void *operator new(size_t, int)
還是
void *operator new(size_t, size_t, int)
如果因下面的語句拋了個異常而被調(diào)用:
p = new(1) T; // calls operator new(size_t, int)
operator delete 的size_t 參數(shù)將是sizeof(T);但如果是被調(diào)用時是
p = new(1, 2) T; // calls operator new(size_t, size_t, int)
operator delete 的size_t 參數(shù)將是new 語句的第一個參數(shù)值(這里是1)。于是,operator
delete 將不知道怎么解釋它的size_t 值。
我估計,你可能想知道是否非placement 的函數(shù)
void operator delete(void *, size_t)
同時作為一個placement 函數(shù)匹配于
void *operator new(size_t, size_t)
如果它被允許,operator delete 將遇到前面講的同樣問題。而不被允許的話, C++標(biāo)
準(zhǔn)將需要其規(guī)則的一個例外。
我沒發(fā)現(xiàn)規(guī)則的這樣一個例外。我試過幾個編譯器,— including EDG’s front end,
my expert witness on such matters — 并認(rèn)為:
void operator delete(void *, size_t)
實際上能同時作為一個placement 釋放函數(shù)和一個非placement 釋放函數(shù)。這是個重要的提
醒。
如果你懷疑我,就將例12 的placement operator delete 移掉。
// Example 13
// ... preamble unchanged
class B
{
// void operator delete(void *const p, int const i)
// {
// std::cout << " B::operator delete(" << p <<
// ", " << i << ")" << std::endl;
// ::operator delete(p);>
// }
// ... rest of class B unchanged
};
// ... class A and main unchanged
現(xiàn)在,類里有一個operator delete 匹配于兩個operator new。其輸出結(jié)果和例12 仍
然相同。(WQ 注:結(jié)論是正確的,但不同的編譯器下對例12 到例14 的反應(yīng)相差很大,很是
有趣?。?br>9.5 結(jié)束
兩個最終要點:
?? 貫穿我整個對::operator new 和B::operator delete 的討論,我總是將函數(shù)申明為非
static。通常這樣的申明意味著有this 指針存在,但這些函數(shù)的行為象它們沒有this
指針。實際上,在這些函數(shù)來試圖引用this,你將發(fā)現(xiàn)代碼不能編譯。不象其它成員
函數(shù),operator new 和operator delete 始終是static 的,即使你沒有用static 關(guān)
鍵字。
?? 無論我在哪兒提到operator new 和operator delete,你都可以用operator new[] 和
operator delete[]代替。相同的模式,相同的規(guī)則,和相同的觀察結(jié)果。(雖然Visual
C++標(biāo)準(zhǔn)運行庫的<new>中缺少operator new[]和operator delete[],編譯器仍然允許
你定義自己的數(shù)組版本。)
我想,這個結(jié)束了我對plcement new 和delete 及它們在處理構(gòu)造函數(shù)拋出的異常時扮
演的角色的解釋。下次,我將介紹給你一個不同的技巧來容忍構(gòu)造函數(shù)拋出的異常。
回到目錄
10. 從私有子對象中產(chǎn)生的異常
幾部分來,我一直展示了一些技巧來捕獲從對象的構(gòu)造函數(shù)中拋出的異常。這些技巧是
在異常從構(gòu)造函數(shù)中漏出來后處理它們。有時,調(diào)用者需要知道這些異常,但通常(如我所
采用的例程中)異常是從調(diào)用者并不關(guān)心的私有子對象中爆發(fā)的。使得用戶要關(guān)心“不可見”
的對象表明了設(shè)計的脆弱。
在歷史上,(可能拋異常)的構(gòu)造函數(shù)的實現(xiàn)者沒有簡單而健壯的解決方法。看這個簡
單的例子:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(new char[count])
{
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
buffer 的構(gòu)造函數(shù)接受字符數(shù)目并從自由空間分配內(nèi)存,然后初始化buffer::p 指向
它。如果分配失敗,構(gòu)造函數(shù)中的new 語句產(chǎn)生一個異常,而buffer 的用戶(這里是main
函數(shù))必須捕獲它。
10.1 try 塊
不幸的是,捕獲這個異常不是件容易事。因為拋出來自buffer::buffer,所有buffer
的構(gòu)造函數(shù)的調(diào)用應(yīng)該被包在try 塊中。沒腦子的解決方法:
try
{
buffer b(count);
}
catch (...)
{
abort();
}
do_something_with(b); // ERROR. At this point,
// 'b' no longer exists
是不行的。do_something_with()的調(diào)用必須在try 塊中:
try
{
buffer b(100);
do_something_with(b);
}
catch (...)
{
abort();
}
//do_something_with(b);
(免得被說閑話:我知道調(diào)用abort()來處理這個異常有些過份。我只是用它做個示例,
因為現(xiàn)在關(guān)心的是捕獲異常而不是處理它。)
雖然有些笨拙,但這個方法是有效的。接著考慮這樣的變化:
static buffer b(100);
int main()
{
// buffer b(100);
do_something_with(b);
return 0;
}
現(xiàn)在,b 被定義為全局對象。試圖將它包入try 塊
try // um, no, I don't think so
{
static buffer b;
}
catch (...)
{
abort();
}
int main()
{
do_something_with(b);
return 0;
}
將不能被編譯。
10.2 暴露實現(xiàn)
每個例子都顯示了buffer 設(shè)計上的基本缺陷:buffer 的接口以外的實現(xiàn)細(xì)節(jié)被暴露了。
在這里,暴露的細(xì)節(jié)是buffer 的構(gòu)造函數(shù)中的new 語句可能失敗。這個語句用于初始化私
有子對象buffer::p――一個main 函數(shù)和其它用戶不能操作甚至根本不知道的子對象。當(dāng)
然,這些用戶更不應(yīng)該被要求必須關(guān)注這樣的子對象拋出的異常。
為了改善buffer 的設(shè)計,我們必須在構(gòu)造函數(shù)中捕獲異常:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char *p;
};
buffer::buffer(size_t const count)
: p(NULL)
{
try
{
p = new char[count];
}
catch (...)
{
abort();
}
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &)
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
異常被包含在構(gòu)造函數(shù)中。用戶,比如main()函數(shù),從不知道異常存在過,世界又一
次清靜了。
10.3 常量成員
也這么做?注意,buffer::p 一旦被設(shè)置過就不能再被改動。為避免指針被無意改動,
謹(jǐn)慎的設(shè)計是將它申明為const:
class buffer
{
public:
explicit buffer(size_t);
~buffer();
private:
char * const p;
};
很好,但到了這步時:
buffer::buffer(size_t const count)
{
try
{
p = new char[count]; // ERROR
}
catch (...)
{
abort();
}
}
一旦被初始化,常量成員不能再被改變,即使是在包含它們的對象的構(gòu)造函數(shù)體中。常
量成員只能被構(gòu)造函數(shù)的成員初始化列表設(shè)置一次。
buffer::buffer(size_t const count)
: p(new char[count]) // OK
這讓我們回到了段落一中,又重新產(chǎn)生了我們最初想解決的問題。
OK,這么樣如何:不用new 語句初始化p,換成用內(nèi)部使用new 的輔助函數(shù)來初始化它:
char *new_chars(size_t const count)
{
try
{
return new char[count];
}
catch (...)
{
abort();
}
}
buffer::buffer(int const count)
: p(new_chars(count))
{
// try
// {
// p = new char[count]; // ERROR
// }
// catch (...)
// {
// abort();
// }
}
這個能工作,但代價是一個額外函數(shù)卻僅僅用來保護一個幾乎從不發(fā)生的事件。
10.4 函數(shù)try 塊
(WQ 注:后面會講到,function try 塊不能阻止構(gòu)造函數(shù)的拋異常動作,它其實只起
異常過濾的功能?。?!見P14.3)
我在上面這些建議中沒有發(fā)現(xiàn)哪個能確實令人滿意。我所期望的是一個語言級的解決方
案來處理部分構(gòu)造子對象問題,而又不引起上面說到的問題。幸運的是,語言中恰好包含了
這樣一個解決方法。
在深思熟慮后,C++標(biāo)準(zhǔn)委員會增加了一個叫做“function try blocks”的東西到語言
規(guī)范中。作為try 塊的堂兄弟,函數(shù)try 塊捕獲整個函數(shù)定義中的異常,包括成員初始化列
表。不用奇怪,因為語言最初沒有被設(shè)計了支持函數(shù)try 塊,所以語法有些怪:
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch
{
abort();
}
看起來想是通常的try 塊后面的{}實際上是劃分構(gòu)造函數(shù)的函數(shù)體的。在效果上,{}有雙重
作用,不然,我們將面對更別扭的東西:
buffer::buffer(int const count)
try
: p(new char[count])
{
{
}
}
catch
{
abort();
}
(注意:雖然嵌套的{}是多余的,這個版本能夠編譯。實際上,你可以嵌套任意重{},
直到遇到編譯器的極限。)
如果在初始化列表中有多個初始化,我們必須將它們放入同一個函數(shù)try 塊中:
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
和普通的try 塊一樣,可以有任意個異常處理函數(shù):
buffer::buffer()
try
: p(...), q(...), r(...)
{
// constructor body
}
catch (std::bad_alloc)
{
// ...
}
catch (int)
{
// ...
}
catch (...)
{
// ...
}
古怪的語法之外,函數(shù)try 塊解決了我們最初的問題:所有從buffer 子對象的構(gòu)造函
數(shù)拋出的異常留在了buffer 的構(gòu)造函數(shù)中。
因為我們現(xiàn)在期望buffer 的構(gòu)造函數(shù)不拋出任何異常,我們應(yīng)該給它一個異常規(guī)格申
明:
explicit buffer(size_t) throw();
接著一想,我們應(yīng)該是個更好點的程序員,于是給我們所有函數(shù)加了異常規(guī)格申明:
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
// ...
};
// ...
static void do_something_with(buffer &) throw()
// ...
Rounding Third and Heading for Home
對我們的例子,最終版本是:
#include <stdlib.h>
class buffer
{
public:
explicit buffer(size_t) throw();
~buffer() throw();
private:
char *const p;
};
buffer::buffer(size_t const count)
try
: p(new char[count])
{
}
catch (...)
{
abort();
}
buffer::~buffer()
{
delete[] p;
}
static void do_something_with(buffer &) throw()
{
}
int main()
{
buffer b(100);
do_something_with(b);
return 0;
}
用Visual C++編譯,自鳴得意地坐下來,看著IDE 的提示輸出。
syntax error : missing ';' before 'try'
syntax error : missing ';' before 'try'
'count' : undeclared identifier
'<Unknown>' : function-style initializer appears
to be a function definition
syntax error : missing ';' before 'catch'
syntax error : missing ';' before '{'
missing function header (old-style formal list?)
噢!
Visual C++還不支持函數(shù)try 塊。在我測試過的編譯器中,只有Edison Design Group
C++ Front End version 2.42 認(rèn)為這些代碼合法。
(順便提一下,我特別關(guān)心為什么編譯將第一個錯誤重復(fù)了一下??赡芩挠嬎隳愕谝?br>次會不相信。)
如果你堅持使用Visual C++,你可以使用在介紹函數(shù)try 塊前所說的解決方法。我喜
歡使用額外的new 封裝函數(shù)。如果你認(rèn)同,考慮將它做成模板:
template <typename T>
T *new_array(size_t const count)
{
try
{
return new T[count];
}
catch (...)
{
abort();
}
}
// ...
buffer::buffer(size_t const count)
: p(new_array<char>(count))
{
}
這個模板比原來的new_chars 函數(shù)通用得多,對char 以外的類型也有能工作。同時,
它有一個隱蔽的異常相關(guān)問題,而我將在下次談到。
回到目錄
11. 異常規(guī)格申明
現(xiàn)在是探索C++標(biāo)準(zhǔn)運行庫和Visual C++在頭文件<exception>中申明的異常支持的時
候了。根據(jù)C++標(biāo)準(zhǔn)(subclause 18.6,“Exception handling” )上的描述,這個頭文件
申明了:
?? 從運行庫中拋出的異常對象的基類。
?? 任何拋出的違背異常規(guī)格申明的對象的可能替代物。
?? 在違背異常規(guī)格申明的異常被拋出是被調(diào)用的函數(shù),以及在其行為上增加?xùn)|西的鉤子
(“hook”)。
?? 在異常處理過程被終止時被調(diào)用的函數(shù),以及在其行為上增加?xùn)|西的鉤子。
我從分析異常規(guī)格申明及程序違背它時遭到什么可怕后果開始。分析將針對上面提到的
主題,以及通常C++異常處理時的一些雜碎。
11.1 異常規(guī)格申明回顧
異常規(guī)格申明是C++函數(shù)申明的一部分,它們指定了函數(shù)可以拋出什么異常。例如,函
數(shù)
void f1() throw(int)
可以拋出一個整型異常,而
void f2() throw(char *, E)
可以拋出一個char *或一個E(這里E 是用戶自定義類型)類型的異常。一個空的規(guī)格申明
void f3() throw()
表明函數(shù)不拋出異常,而沒有規(guī)格申明
void f4()
表明函數(shù)可以拋出任何東西。注意語法
void f4() throw(...)
比前面的“拋任何東西”的函數(shù)更好,因為它類似“捕獲任何東西”
catch(...)
然而,認(rèn)可“拋任何東西” 的函數(shù)就允許了那些在異常規(guī)格申明存在前寫下的函數(shù)。
11.2 違背異常規(guī)格申明
迄今為止,我寫的都是:函數(shù)可能拋出在它的異常規(guī)格申明中描述的異常?!翱赡堋庇?br>些單薄,“必須”則有力些?!翱赡堋北硎玖撕瘮?shù)可以忽略它們的異常規(guī)格。你也許認(rèn)為編譯
器將禁止這種行為:
void f() throw() // Promises not to throw...
{
throw 1; // ...but does anyway - error?
}
但你錯了。用Visual C++試一下,你將發(fā)現(xiàn)編譯器保持沉默,它沒有發(fā)現(xiàn)編譯期錯誤。
實際上,在我所用過的編譯器中,沒有一個報了編譯期錯誤。
話雖這么說,但異常規(guī)格申明有它的規(guī)則的,函數(shù)違背它將遭受嚴(yán)重后果的。不幸的是,
這些后果表現(xiàn)在運行期錯誤而不是編譯期。想看的話,把上面的小段代碼放到一個完整程序
中:
void f() throw()
{
throw 1;
}
int main()
{
f();
return 0;
}
當(dāng)程序運行時將發(fā)生什么?f()拋出一個int 型異常,違背了它的契約。你可能認(rèn)為這
個異常將從main()中漏入運行期庫?;谶@個假設(shè),你傾向于使用一個簡單的try 塊:
#include <stdio.h>
void f() throw()
{
throw 1;
}
int main()
{
try
{
f();
}
catch (int)
{
printf("caught int ");
}
return 0;
}
來捕獲這個異常,以防止它漏出去。
實際上,如果你用Visual C++ 6 編譯并運行,你將得到:
caught int
你再次奇怪throw()異常規(guī)格實際做了什么有用的事,除了增加了源代碼的大小和看起
來比較快感。你的奇怪感覺將變得遲鈍,只要一回想到前面說了多少Visual C++違背C++
標(biāo)準(zhǔn)的地方,只不過再多一個新問題:Visaul C++正確地處理了違背異常規(guī)格申明的情況了
嗎?
11.3 調(diào)查說明……
沒有!
這個程序的行為符合標(biāo)準(zhǔn)嗎?catch 語句不該進入的。來自于標(biāo)準(zhǔn)(subclauses 15.5.2
and 18.6.2.2):
?? 一個異常規(guī)格申明保證只有被列出的異常被拋出。
?? 如果帶異常規(guī)格申明的函數(shù)拋出了一個沒有列出的異常,函數(shù)
?? void unexpected()在退完棧后立即被調(diào)用。
?? 函數(shù)unexpected()將不會返回……
當(dāng)一個函數(shù)試圖拋出沒有列出的異常時,通過unexpected()函數(shù)調(diào)用了一個異常處理
函數(shù)。這個異常處理函數(shù)的默認(rèn)實現(xiàn)是調(diào)用terminate() 來結(jié)束程序。
在我給你一個簡短的例程后,我將展示Visual C++的行為怎么樣地和標(biāo)準(zhǔn)不同。
11.4 unexpected()函數(shù)指南
unexpected()函數(shù)是標(biāo)準(zhǔn)運行庫在頭文件<exception>中申明的函數(shù)。和其它大部分運
行庫函數(shù)一樣,unexpected()函數(shù)存在于命名空間std 中。它不接受參數(shù),也不返回任何東
西,實際上unexpected()函數(shù)從不返回,就象abort()和exit()一樣。如果一個函數(shù)違背
了它自己的異常規(guī)格申明,unexpected()函數(shù)在退完棧后被立即調(diào)用。
基于我對標(biāo)準(zhǔn)的理解,運行庫的unexpected()函數(shù)的實現(xiàn)理論上是這樣的:
void _default_unexpected_handler_()
{
std::terminate();
}
std::unexpected_handler _unexpected_handler =
_default_unexpected_handler;
void unexpected()
{
_unexpected_handler();
}
(_default_unexpected_handler 和_unexpected_handler 是我虛構(gòu)的名字。你的運行
庫的實現(xiàn)可能使用其它名稱,完全取決于其實現(xiàn)。 )
std::unexpected()調(diào)用一個函數(shù)來真正處理unexpected 的異常。它通過一個隱藏的指
針(_unexpected_handler,類型是std::unexpected_handler)來引用這個處理函數(shù)的。
運行庫提供了一個默認(rèn)處理函數(shù)( default_unexpected_handler() ), 它調(diào)用
std::terminate()來結(jié)束程序。
因為是通過指針_unexpected_handler 間接調(diào)用的, 你可以將內(nèi)置的調(diào)用
_default_unexpected_handler 改為調(diào)用你自己的處理函數(shù),只要這個處理函數(shù)的類型兼容
于std::unexpected_handler:
typedef void (*unexpected_handler)();
同樣,處理函數(shù)必須不返回到它的調(diào)用者(std::unexpected())中。沒人阻止你寫一
個會返回的處理函數(shù),但這樣的處理函數(shù)不是標(biāo)準(zhǔn)兼容的,其結(jié)果是程序的行為有些病態(tài)。
你可以通過標(biāo)準(zhǔn)運行庫的函數(shù)std::set_unexpected()來掛接自己的處理函數(shù)。注意,
運行庫只維護一個處理函數(shù)來處理所有的unexpected 異常;一旦你調(diào)用了set_unexpected()
函數(shù),運行庫將不再記得前一次的處理函數(shù)。(和atexit()比較一下,atexit()至少可以掛
32 重exit 處理函數(shù)。)要克服這個限制,你要么在不同的時間設(shè)置不同的處理函數(shù),要么
使你的處理函數(shù)在不同的上下文時有不同的行為。
11.5 Visual C++ vs unexpected
試一下這個簡單的例子:
#include <exception>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
void my_unexpected_handler()
{
printf("in unexpected handler ");
abort();
}
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
set_unexpected(my_unexpected_handler);
throw_unexpected_exception();
printf("this line should never appear ");
return 0;
}
用一個標(biāo)準(zhǔn)兼容的編譯器編譯并運行,程序結(jié)果是:
in unexpected handler
可能接下來是個異常異常終止的特殊(因為有abort()的調(diào)用)。但用Visual C++編譯并運
行,程序會拋出“Unhandled exception”對話框。關(guān)閉對話框后,程序輸出:
this line should never appear
必須承認(rèn),Visual C++沒有正確實現(xiàn)unexpected()。這個函數(shù)被申明在<exception>中,
運行期庫中有其實現(xiàn),只不過這個實現(xiàn)不做任何事。
實際上,Visual C++甚至沒有正確地申明,用這個理論上等價的程序可以證明:
#include <exception>
#include <stdio.h>
#include <stdlib.h>
//using namespace std;
void my_unexpected_handler()
{
printf("in unexpected handler ");
abort();
}
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
std::set_unexpected(my_unexpected_handler);
throw_unexpected_exception();
printf("this line should never appear ");
return 0;
}
Visual C++不能編譯這個程序。查看<exception>表明:set_unexpected_handler()被
申明為全局函數(shù)而不是在命名空間std 中。實際上,所有的unexpected 族函數(shù)都被申明為
全局函數(shù)。
底線:Visual c++能編譯使用unexpected()等函數(shù)的程序,但運行時的行為是不正確
的。
我希望Microsoft 能在下一版中改正這些問題。在未改正前,當(dāng)討論涉及到unexpected()
時,我建議你使用標(biāo)準(zhǔn)兼容的C++編譯器。
11.6 維持程序存活
在我所展示的簡單例子中,程序在my_unexpected_handler()里停止了。有時,讓程序
停止是合理和正確的;但更多情況下,程序停止是太刺激了,尤其是當(dāng)unexpected 異常表
明的是程序只輕微錯誤。
假定你想處理unexpected 異常,并恢復(fù)程序,就象對大多數(shù)其它“正?!碑惓R粯印?br>因為unexpected()從不返回,程序恢復(fù)似乎不可能,除非你看了標(biāo)準(zhǔn)的subclause 15.5.2:
unexpected()不該返回,但它可以throw(或re-throw)一個異常。如果它拋出一個新
異常,而這異常是異常規(guī)格申明允許的,搜索另外一個異常處理函數(shù)的行為在調(diào)用
unexpected()的地方繼續(xù)進行。
太好了!如果my_unexpected_handler()拋出一個允許的異常,程序就能從最初的違背
異常規(guī)格申明的地方恢復(fù)了。在我們的例子里,最初的異常規(guī)格申明允許int 型的異常。根
據(jù)上面的說法,如果my_unexpected_handler 拋出一個int 異常,程序?qū)⒛芾^續(xù)了。
基于這種猜測,試一下:
#include <exception>
#include <stdio.h>
void my_unexpected_handler()
{
printf("in unexpected handler ");
throw 2; // allowed by original specification
//abort();
}
用標(biāo)準(zhǔn)兼容的編譯器編譯運行,程序輸出:
in unexpected handler
program resumed
和期望相符。
拋出的int 異常和其它異常一樣順調(diào)用鏈傳遞,并被第一個相匹配的異常處理函數(shù)捕
獲。在我們的例子里,程序的控制權(quán)從my_unexpected_handler()向std::unexpected()再
向main()回退,并在main()中捕獲異常。用這種方法,my_unexpected_handler()變成了一
個異常轉(zhuǎn)換器,將一個最初的“壞”的long 型異常轉(zhuǎn)換為一個“好”的int 型異常。
結(jié)論:通過轉(zhuǎn)換一個unexpected 異常為expected 異常,你能恢復(fù)程序的運行。
11.7 預(yù)告
下次,我將結(jié)束std::unexpected()的討論:揭示在my_unexpected_handler()中拋異
常的限制,探索運行庫對這些限制的補救,并給出處理unexpected 異常的通行指導(dǎo)原則。
我也將開始討論運行庫函數(shù)std::terminate()的相關(guān)內(nèi)容。
void throw_unexpected_exception() throw(int)
{
throw 1L; // violates specification
}
int main()
{
std::set_unexpected(my_unexpected_handler);
try
{
throw_unexpected_exception();
printf("this line should never appear ");
}
catch (int)
{
printf("program resumed ");
}
return 0;
}
回到目錄
12. unexpected()的實現(xiàn)上固有的限制
上次,我介紹了C++標(biāo)準(zhǔn)運行庫函數(shù)unexpected(),并展示了Visual C++的實現(xiàn)版本
中的限制。這次,我想展示所有unexpected()的實現(xiàn)上固有的限制,以及繞開它們的辦法。
12.1 異常處理函數(shù)是全局的、通用的
我在上次簡要地提過這點,再推廣一點:過濾unexpected 異常的異常處理函數(shù)
unexpected()是全局的,對每個程序是唯一的。
所有unexpected 異常都被同樣的一個unexpected()異常處理函數(shù)處理。標(biāo)準(zhǔn)運行庫提
供默認(rèn)的處理函數(shù)來處理所有unexpected 異常。你可以用自己的版本覆蓋它,這時,運行
庫會調(diào)用你提供的處理函數(shù)來處理所有的unexpected 異常。
和普通的異常處理函數(shù),如:
catch (int)
{
}
不同,unexpected 異常處理函數(shù)不“捕獲”異常。一旦被進入,它就知道有unexpected 異
常被拋出,但不知道類型和起因,甚至沒法得到運行庫的幫助:運行庫中沒有程序或?qū)ο蟊?br>存這些討厭的異常。
在最好的情況下,unexpected 異常處理函數(shù)可以把控制權(quán)交給程序的其它部分,也許
它們有更好的辦法。例如:
#include <exception>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(int)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexpected_handler);
try
{
f();
}
catch (...)
{
}
return 0;
}
f()拋出了一個它承諾不拋的異常,于是my_unexpected_handler()被調(diào)用。這個處理
函數(shù)沒有任何辦法來判斷它被進入的原因。除了結(jié)束程序外,它唯一可能有些用的辦法是拋
出另外一個異常,希望新異常滿足被老異常違背的異常規(guī)格申明,并且程序的其它部分將捕
獲這個新異常。
在這個例子里,my_unexpected_handler()拋出的int 異常滿足老異常違背的異常規(guī)格
申明,并且main()成功地捕獲了它。但稍作變化:
#include <exception>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(char)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexepected_handler);
try
{
f();
}
catch (...)
{
}
return 0;
}
my_unexpected_handler()仍然在unexpected 異常發(fā)生后被調(diào)用,并仍然拋出了一個
int 型異常。不幸的是,int 型異常現(xiàn)在和老異常違背的異常規(guī)格申明相違背。因此,我們
現(xiàn)在兩次違背了同一異常規(guī)格申明: 第一次是f() , 第二次是f() 的援助者
my_unexpected_handler()。
12.2 Terminate
現(xiàn)在,程序放棄了,并調(diào)用運行庫的程序terminate()自毀。terminate()函數(shù)是標(biāo)準(zhǔn)
運行庫在異常處理上的最后一道防線。當(dāng)程序的異常處理體系感到無望時,C++標(biāo)準(zhǔn)要求程
序調(diào)用terminate()函數(shù)。C++標(biāo)準(zhǔn)的Subclause 15.5.1 列出了調(diào)用terminate()的情況:
和unexpected() 處理函數(shù)一樣, terminate() 處理函數(shù)也可以用戶定義。但和
unexpected()處理函數(shù)不同的是,terminate()處理函數(shù)必須結(jié)束程序。記住:當(dāng)你的
terminate()處理函數(shù)被進入時,異常處理體系已經(jīng)無效了,此是程序所需要的最后一件事
是找一個terminate()處理函數(shù)來丟棄異常。
在能避免時就不要讓你的程序調(diào)用terminate()。terminate()其實是個叫得好聽點的
exit()。如果terminate()被調(diào)用了,你的程序就會以一種不愉快的方式死亡。
就如同不能完全支持unexpected()一樣,Visual c++也不能完全支持terminate()。要
在實際運行中驗證的話,運行:
#include <exception>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
void my_terminate_handler()
{
printf("in my_terminate_handler ");
abort();
}
int main()
{
set_terminate(my_terminate_handler);
throw 1; // nobody catches this
return 0;
}
根據(jù)C++標(biāo)準(zhǔn),拋出了一個沒人捕獲的異常將導(dǎo)致調(diào)用terminate()(這是我前面提到
的Subclause 15.5.1 中列舉的情況之一)。于是,上面的程序一個輸出:
in my_terminate_handler
但,用Visual C++編譯并運行,程序沒有輸出任何東西。
12.3 避免terminate
在我們的unexpected()例子中,terminate()最終被調(diào)用是因為f()拋出了unexpected
異常。我們的unexpected_handler()試圖阻住這個不愉快的事,通過拋出一個新異常,但
沒成功;這個拋出行為因再度產(chǎn)生它試圖解決的那個問題而結(jié)束。我們需要找到一個方法以
使得unexpected()處理函數(shù)將控制權(quán)傳給程序的其它部分(假定那部分程序是足夠聰明的,
能夠成功處掉異常)而不導(dǎo)致程序終止。
很高興,C++標(biāo)準(zhǔn)正好提供了這樣一個方法。如我們所看過的,從unexpected()處理函
數(shù)中拋出的異常對象必須符合(老異常違背的)異常規(guī)格申明。這個規(guī)則有一個例外:如果
如果被違背的異常規(guī)格申明中包含類型bad_exception,一個bad_exception 對象將替代
unexpected()處理函數(shù)拋出的對象。例如:
#include <exception>
#include <stdio.h>
using namespace std;
void my_unexpected_handler()
{
throw 1;
}
void f() throw(char, bad_exception)
{
throw 1L; // oops -- *bad* function
}
int main()
{
set_unexpected(my_unexpected_handler);
try
{
f();
}
catch (bad_exception const &)
{
printf("caught bad_exception ");
// ... even though such an exception was never thrown
}
return 0;
}
當(dāng)用C++標(biāo)準(zhǔn)兼容的編譯器編譯并運行,程序輸出:
caught bad_exception
當(dāng)用Visual C++編譯并運行,程序沒輸出任何東西。因為Visual c++并沒有在第一次
拋異常的地方捕獲unexpected 異常,它沒有機會進行bad_exception 的替換。
和前面的例子相同的是,f()仍然違背它的異常規(guī)格申明,而my_unexpected_handler()
仍然拋出一個int。不同之處是:f()的異常規(guī)格申明包含bad_exception。結(jié)果,程序悄悄
地將my_unexpected_handler()原來拋出的int 對象替換為bad_exception 對象。因為
bad_exception 異常是允許的,terminate()沒有被調(diào)用,并且這個bad_exception 異常能
被程序的其它部分捕獲。
最終結(jié)果:最初從f()拋出的long 異常先被映射為int,再被映射為bad_exception。
這樣的映射不但避免了前面導(dǎo)致terminate 的再次異常問題,還給程序的其它部分一個修正
的機會。bad_exception 異常對象的存在表明了某處最初拋出了一個unexpected 異常。通
過在問題點附近捕獲這樣的對象,程序可以得體地恢復(fù)。
我也注意到一個奇怪的地方。在代碼里, 你看到f() 拋出了一個long ,
my_unexpected_handler()拋出了一個int,而沒人拋出bad_exception,但main()確實捕
獲到一個bad_exception。是的,程序捕獲了一個它從沒拋出的對象。就我所知,唯一被允
許發(fā)生這種行為的地方就是unexpected 異常處理函數(shù)和bad_exception 異常間的相互作用。
12.4 一個更特別的函數(shù)
C++標(biāo)準(zhǔn)定義了3 個“特別”函數(shù)來捕獲異常。其中,你已經(jīng)看到了terminate()和
unexpected()。最后,也是最簡單的一個是uncaght_exception()。摘自C++標(biāo)準(zhǔn)(15.5.3):
函數(shù)bool uncaught_exception()在被拋出的異常對象完成賦值到匹配的異常處理函數(shù)
的異常申明完成初始化之間返回true。包括其中的退棧過程。如果異常被再次拋出,
uncaught_exception() 從再拋點到再拋對象被再次捕獲間返回true。
uncaught_exception() 讓你查看是否程序拋出了一個異常而還沒有被捕獲。這個函數(shù)
對析構(gòu)函數(shù)有特別意義:
#include <exception>
#include <stdio.h>
using namespace std;
class X
{
public:
~X();
};
X::~X()
{
if (uncaught_exception())
printf("X::~X called during stack unwind ");
else
printf("X::~X called normally ");
}
int main()
{
X x1;
try
{
X x2;
throw 1;
}
catch (...)
{
}
return 0;
}
在C++標(biāo)準(zhǔn)兼容的環(huán)境下,程序輸出:
X::~X called during stack unwind
X::~X called normally
x1 和x2 在main()拋出異常前構(gòu)造。退棧時調(diào)用x2 的析構(gòu)函數(shù)。因為一個未被捕獲的
異常在析構(gòu)函數(shù)調(diào)用期間處于活動狀態(tài),uncaught_exception()返回true。然后,x1 的析
構(gòu)函數(shù)被調(diào)用(在main()退出時),異常已經(jīng)恢復(fù),uncaught_exception()返回false。
和以前一樣,Visual C++在這里也不支持C++標(biāo)準(zhǔn)。在其下編譯,程序輸出:
X::~X called normally
X::~X called normally
如果你了解Microsoft 的SEH(我在第二部分講過的),就知道uncaught_exception()
類似于SEH 的AbnormalTermination()。在它們各自的應(yīng)用范圍內(nèi),兩個函數(shù)都是檢測是否
一個被拋出的異常處于活動狀態(tài)而仍然沒有被捕獲。
12.5 小結(jié)
大多數(shù)函數(shù)不直接拋異常,但將其它函數(shù)拋的異常傳遞出來。決定哪些異常被傳遞是非
常困難的,尤其是來自于沒有異常規(guī)格申明的函數(shù)的。bad_exception ()是一個安全的閥門,
提供了一個方法來保護那些你不能進行完全解析的異常。
這些保護能工作,但,和普通的異常處理函數(shù)一樣,需要你明確地設(shè)計它。對每個可能
違背其異常規(guī)格申明的函數(shù),你都必須記得在其異常規(guī)格申明中加一個bad_exception 并在
某處捕獲它。bad_exception 和其它異常沒有什么不同:如果你不想捕獲它,不去產(chǎn)生它就
行了。一個沒有并捕獲的bad_exception 將導(dǎo)致程序終止,就象在最初的地方你沒有使用
bad_exception 進行替換一樣。
異常規(guī)格申明使你意圖明確。它說“這是我允許這個函數(shù)拋出的異常的集合;如果函數(shù)
拋出了其它東西,不是我的設(shè)計錯了就是程序有神經(jīng)病(the program is buggy)”。一個
unexpected 異常,不管它怎么出現(xiàn)的,都表明了一個邏輯錯誤。我建議你最好讓錯誤以一
種可預(yù)見的方式有限度地發(fā)生。
所有這些表明你可以描繪你的代碼在最開始時的異常的行為。不幸的是,這樣的描繪接
近于巫術(shù)。下次,我將給出一些指導(dǎo)方針來分析你的代碼中的異常。
回到目錄
13. 異常安全
接下來兩次,我將討論“異常安全”,C++標(biāo)準(zhǔn)中使用了(在auto_ptr 中)卻沒有定義
的術(shù)語。在C++范圍內(nèi),不同的作者使用這個術(shù)語卻表達(dá)不同的含義。在我的專題中,我從
兩個方面來定義“異常安全”:
?? 如果一個實體捕獲或拋出一個異常,但仍然維持它公開保證的語義,它就是“接口安全”
的。依賴于它保證的力度,實體可能不允許將任何異常漏給其用戶。
?? 如果異常沒有導(dǎo)致資源泄漏或產(chǎn)生未定義的行為,實體就是“行為安全”的?!靶袨榘?br>全”一般是強迫的。幸運的是,如果做到了“行為安全”,通常也間接提供了“接口安
全”。
異常安全有點象const:好的設(shè)計必須在一開始就考慮它,它不能夠事后補救。但是我
們開始使用異常還沒有多少年,所以還沒有“異常安全問題集”這樣的東西來指導(dǎo)我們。實
際上,我期望大家通過一條艱辛的道路來掌握異常安全:通過經(jīng)歷異常故障在編碼時繞過它
們;或關(guān)閉異常特性,認(rèn)為它們“太難”被正確掌握。
我不想撒謊:分析設(shè)計上的異常安全性太難了。但是,艱辛的工作也有豐厚的回報。不
過,這個主題太難了,想面面俱到的話將花我?guī)讉€月的時間。我最小的目標(biāo)是:通過缺乏異
常安全的例子來展示怎么使它們變得安全,并激勵你在此專題之外去看和學(xué)更多的東西。
13.1 構(gòu)造函數(shù)
如果一個普通的成員函數(shù)
x.f()
拋出一個異常,你可以容忍此異常并試圖再次調(diào)用它:
X x;
bool done;
do
{
try
{
done = true;
x.f();
}
catch (...)
{
// do something to recover, then retry
done = false;
}
}
while (!done);
但,如果你試圖再次調(diào)用一個構(gòu)造函數(shù),你實際上是調(diào)用了一個完全不同的對象:
bool done(false);
while (!done)
{
try
{
done = true;
X x; // calls X::X()
}
// from this point forward, `x` does not exist
catch (...)
{
// do something to recover, then retry
done = false;
}
}
你不能挽救一個構(gòu)造函數(shù)拋異常的對象;異常的存在表明那個對象已經(jīng)死了。
當(dāng)一個構(gòu)造函數(shù)拋異常時,它殺死了其宿主對象而沒有調(diào)用析構(gòu)函數(shù)。這樣的拋異常行
為危害了“行為安全”:如果這個拋異常的構(gòu)造函數(shù)分配了資源,你無法依賴析構(gòu)函數(shù)釋放
它們。一般構(gòu)造和析構(gòu)是成對的,并期待后者清理前者。如果析構(gòu)函數(shù)沒有被調(diào)用,這個期
望是不滿足的。
最后,如果你從構(gòu)造函數(shù)中拋了一個異常,并且你的類是用戶類的一個基類或子對象,
那么用戶類的構(gòu)造函數(shù)必須處理你拋出的異常?;蛘咚鼘惓伣o另外一個用戶類的構(gòu)造函
數(shù),如此遞推下去,直到程序調(diào)用terminate()。實際上用戶必須做你沒有做的工作(維持
構(gòu)造函數(shù)的安全性)。
13.2 關(guān)于取舍的問題
構(gòu)造函數(shù)拋異常同時降低了接口安全和行為安全。除非有迫不得以的理由,不要讓構(gòu)造
函數(shù)拋異常。
也有不同的意見認(rèn)為:異常應(yīng)該被本來就做這事的專門代碼捕獲的。那些只是靜靜地接
收異常而沒有處理它們的異常處理函數(shù)違背了這些異常的初衷。如果一個函數(shù)沒有準(zhǔn)備好正
確地處理一個異常,它應(yīng)該將這個異常傳遞下去。
最低事實是:必須有人處理異常;如果所有人都放過它,程序?qū)⒔K止。還必須同時捕獲
觸發(fā)異常的條件;如果沒人標(biāo)記它,程序可能以任何方式終止,并且恐怕不怎么文雅。
一個異常對象警示我們存在一個不該忽略的錯誤狀況。不幸的是,這個對象的存在可能
導(dǎo)致一個全新的不同的錯誤狀況。在設(shè)計異常安全的時候,你必須在兩個有時沖突的設(shè)計原
則間進行取舍。
1.在錯誤發(fā)生時進行通報
2.防止這個通報行為導(dǎo)致其它錯誤。
因為構(gòu)造函數(shù)拋異??赡苡杏泻Φ母弊饔茫惚仨毿⌒臋?quán)衡這兩個原則。我不允許我寫
的構(gòu)造函數(shù)中拋異常,這樣設(shè)計傾向于原則2;但我不想將它推薦為普遍原則,在其它情況
下這兩個原則是等重的。自己好自判斷吧。
13.3 析構(gòu)函數(shù)
析構(gòu)函數(shù)拋異??赡苁钩绦蛴衅婀值姆磻?yīng)。它可能徹底地殺死程序。根據(jù)C++標(biāo)準(zhǔn)
(subclause 15.1.1,“the terminate() function” ),簡述如下:
在某些情況下,異常處理必須被拋棄以減少一些微妙的錯誤。這些情況中包括:當(dāng)因為
異常而退棧過程中將要被析構(gòu)的對象的析構(gòu)函數(shù)。在這些情況下,函數(shù)void terminate()
被調(diào)用。退棧不會完成。
簡而言之,析構(gòu)函數(shù)不該提示是否發(fā)生了異常。但,如我上次所說,新的C++標(biāo)準(zhǔn)運行
庫程序uncaught_exception()可以讓析構(gòu)函數(shù)確定其所處的異常環(huán)境。不幸的是,我上次
也說了,Visual C++未能正確地支持這個函數(shù)。
問題比我提示的還要糟。我上次寫到,Microsoft 的uncaught_exception()函數(shù)版本一
定返回false,所以Visaul C++總告訴你的析構(gòu)函數(shù)當(dāng)前沒有發(fā)生異常,在其中拋異常是可
以的。如果你從一個支持uncaught_exception 的環(huán)境轉(zhuǎn)到Visual C++,以前正常工作的代
碼可能開始調(diào)用terminate()了。
要嘗試一下的話,試下面的例子:
#include <exception>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
static void my_terminate_handler(void)
{
printf("Library lied; I'm in the terminate handler. ");
abort();
}
class X
{
public:
~X()
{
if (uncaught_exception())
printf("Library says not to throw. ");
else
{
printf("Library says I'm OK to throw. ");
throw 0;
}
}
};
int main()
{
set_terminate(my_terminate_handler);
try
{
X x;
throw 0;
}
catch (...)
{
}
printf("Exiting normally. ");
return 0;
}
在C++標(biāo)準(zhǔn)兼容的環(huán)境下,你得到:
Library says not to throw.
Exiting normally.
但Visual C++下,你得到:
Library says I'm OK to throw.
Library lied; I'm in the terminate handler.
并跟隨一個程序異常終止。
And with six you get egg roll.
建議:除非你確切知道你現(xiàn)在及以后所用的平臺都正確支持uncaught_exception(),
不要調(diào)用它。
13.4 部分刪除
即使你知道當(dāng)前不在處理異常,你仍然不應(yīng)該在析構(gòu)函數(shù)中拋異常??紤]如下的例子:
class X
{
public:
~X()
{
throw 0;
}
};
int main()
{
X *x = new X;
delete x;
return 0;
}
當(dāng)main()執(zhí)行到delete x,如下兩步將依次發(fā)生:
x 的析構(gòu)函數(shù)被調(diào)用。
operator delete 被調(diào)用了來釋放x 的內(nèi)存空間。
但因為x 的析構(gòu)函數(shù)拋了異常,operator delete 沒有被調(diào)用。這危及了行為安全。如
果還不信,試一下這個更完整的例子:
#include <stdio.h>
#include <stdlib.h>
class X
{
public:
~X()
{
printf("destructor ");
throw 0;
}
void *operator new(size_t n) throw()
{
printf("new ");
return malloc(n);
}
void operator delete(void *p) throw()
{
printf("delete ");
if (p != NULL)
free(p);
}
};
int main()
{
X *x = new X;
try
{
delete x;
}
catch (...)
{
printf("catch ");
}
return 0;
}
如果析構(gòu)函數(shù)沒有拋異常,程序輸出:
new
destructor
delete
實際上程序輸出:
new
destructor
catch
operator delete 沒有進入,x 的內(nèi)存空間沒有被釋放,程序有資源泄漏,the press
hammers your product for eating memory, and you go back to flipping burgers for a
living。
原則:異常安全要求你不能在析構(gòu)函數(shù)中拋異常。和在構(gòu)造函數(shù)拋異常上有不同意見不
一樣,這條是絕對的。為了明確表明意圖,應(yīng)該在申明析構(gòu)函數(shù)時加上異常規(guī)格申明
throw()。
13.5 預(yù)告
我本準(zhǔn)備覆蓋模板安全的,但沒地方了。我將留到下次介紹,并開出推薦讀物表。
回到目錄
14. 模板安全
上次,我開始討論異常安全。這次,我將探究模板安全。
模板根據(jù)參數(shù)的類型進行實例化。因為通常事先不知道其具體類型,所以也無法確切知
道將在哪兒產(chǎn)生異常。你大概最期望的就是去發(fā)現(xiàn)可能在哪兒拋異常。這樣的行為很具挑戰(zhàn)
性。
看一下這個簡單的模板類:
template <typename T>
class wrapper
{
public:
wrapper()
{
}
T get()
{
return value_;
}
void set(T const &value)
{
value_ = value;
}
private:
T value_;
wrapper(wrapper const &);
wrapper &operator=(wrapper const &);
};
如名所示,wrapper 包容了一個T 類型的對象。方法get()和set()得到和改變私有的
包容對象value_。兩個常用方法--拷貝構(gòu)造函數(shù)和賦值運算符沒有使用,所以沒有定義,
而第三個--析構(gòu)函數(shù)由編譯器隱含定義。
實例化的過程很簡單,例如:
wrapper<int> i;
包容了一個int。i 的定義過程導(dǎo)致編譯器從模板實例化了一個定義為wrapper<int>的類:
template <>
class wrapper<int>
{
public:
wrapper()
{
}
int get()
{
return value_;
}
void set(int const &value)
{
value_ = value;
}
private:
int value_;
wrapper(wrapper const &);
wrapper &operator=(wrapper const &);
};
因為wrapper<int>只接受int 或其引用(一個內(nèi)嵌類型或內(nèi)嵌類型的引用),所以不會
觸及異常。wrapper<int>不拋異常,也沒有直接或間接調(diào)用任何可能拋異常的函數(shù)。我不進
行正規(guī)的分析了,但相信我:wrapper<int>是異常安全的。
14.1 class 類型的參數(shù)
現(xiàn)在看:
wrapper<X> x;
這里X 是一個類。在這個定義里,編譯器實例化了類wrapper<X>:
template <>
class wrapper<X>
{
public:
wrapper()
{
}
X get()
{
return value_;
}
void set(X const &value)
{
value_ = value;
}
private:
X value_;
wrapper(wrapper const &);
wrapper &operator=(wrapper const &);
};
粗一看,這個定義沒什么問題,沒有觸及異常。但思考一下:
?? wrapper<X>包容了一個X 的子對象。這個子對象需要構(gòu)造,意味著調(diào)用了X 的默認(rèn)構(gòu)造
函數(shù)。這個構(gòu)造函數(shù)可能拋異常。
?? wrapper<X>::get()產(chǎn)生并返回了一個X 的臨時對象。為了構(gòu)造這個臨時對象,get()
調(diào)用了X 的拷貝構(gòu)造函數(shù)。這個構(gòu)造函數(shù)可能拋異常。
?? wrapper<X>::set()執(zhí)行了表達(dá)式value_ = value,它實際上調(diào)用了X 的賦值運算。這
個運算可能拋異常。
在wrapper<int>中針對不拋異常的內(nèi)嵌類型的操作現(xiàn)在在wrapper<X>中變成調(diào)用可能
拋異常的函數(shù)了,同樣的模板,同樣的語句,但極其不同的含義。
由于這樣的不確定性,我們需要采用保守的策略:假設(shè)wrapper 會根據(jù)類來實例化,而
這些類在其成員上沒有異常規(guī)格申明,它們可能拋異常。
14.2 使得包容安全
再假設(shè)wrapper 的異常規(guī)格申明承諾其成員不產(chǎn)生異常。至少,我們必須在其成員上加
上異常規(guī)格申明throw()。我們需要修補掉這些可能導(dǎo)致異常的地方:
?? 在wrapper::wrapper()中構(gòu)造value_的過程。
?? 在wrapper::get()中返回value_的過程。
?? 在wrapper::set()中對value_賦值的過程。
另外,在違背throw()的異常規(guī)格申明時,我們還要處理std::unexpected。
14.3 Leak #1:默認(rèn)構(gòu)造函數(shù)
對wrapper 的默認(rèn)構(gòu)造函數(shù),解決方法看起來是采用function try 塊:
wrapper() throw()
try : T()
{
}
catch (...)
{
}
雖然很吸引人,但它不能工作。根據(jù)C++標(biāo)準(zhǔn)(paragraph 15.3/16,“Handling an
exception”):
對構(gòu)造或析構(gòu)函數(shù)上的function-try-block,當(dāng)控制權(quán)到達(dá)了異常處理函數(shù)的結(jié)束點
時,被捕獲的異常被再次拋出。對于一般的函數(shù),此時是函數(shù)返回,等同于沒有返回值的
return 語句,對于定義了返回類型的函數(shù)此時的行為為未定義。
換句話說,上面的程序相當(dāng)于是:
X::X() throw()
try : T()
{
}
catch (...)
{
throw;
}
這不是我們想要的。
我想過這樣做:
X::X() throw()
try
{
}
catch (...)
{
return;
}
但它違背了標(biāo)準(zhǔn)的paragraph 15:
如果在構(gòu)造函數(shù)上的function-try-block 的異常處理函數(shù)體中出現(xiàn)了return 語句,程
序是病態(tài)的。
我被標(biāo)準(zhǔn)卡死了,在用支持function try 塊的編譯器試驗后,我沒有找到讓它們以我
所期望的方式運行的方法。不管我怎么嘗試,所有被捕獲的異常都仍然被再次拋出,違背了
throw()的異常規(guī)格申明,并打敗了我實現(xiàn)接口安全的目標(biāo)。
原則:無法用function try 塊來實現(xiàn)構(gòu)造函數(shù)的接口安全。
引申原則1:盡可能使用構(gòu)造函數(shù)不拋異常的基類或成員子對象。
引申原則2:為了幫助別人實現(xiàn)引申原則1,不要從你的構(gòu)造函數(shù)中拋出任何異常。(這
和我在Part13 中所提的看法是矛盾的。)
我發(fā)現(xiàn)C++標(biāo)準(zhǔn)的規(guī)則非常奇怪,因為它們減弱了function try 的實際價值:在進入
包容對象的構(gòu)造函數(shù)(wrapper::wrapper())前捕獲從子對象(T::T())構(gòu)造函數(shù)中拋出的
異常。實際上,function try 塊是你捕獲這樣的異常的唯一方法;但是你只能捕獲它們卻
不能處理掉它們!
(WQ 注:下面的文字原載于Part15 上,我把提前了。
上次我討論了function try 塊的局限性,并承諾要探究其原因的。我所聯(lián)系的業(yè)內(nèi)專
家沒人知道確切答案?,F(xiàn)在唯一的共識是:
?? 如我所猜測,標(biāo)準(zhǔn)委員會將function try 塊設(shè)計為過濾而不是捕獲子對象構(gòu)造函數(shù)中
發(fā)生的異常的。
?? 可能的動機是:確保沒人誤用沒有構(gòu)造成功的包容對象。
我寫信給了Herb Sutter,《teh Exceptional C++》的作者。他從沒碰過這個問題,但
很感興趣,以至于將其寫入“Guru of the Week”專欄。如果你想加入這個討論,到新聞組
comp.lang.c++.moderated 上去看“Guru of the Week #66: Constructor Failures”。

注意function try 可以映射或轉(zhuǎn)換異常:
X::X()
try
{
throw 1;
}
catch (int)
{
throw 1L; // map int exception to long exception
}
這樣看,它們非常象unexpected 異常的處理函數(shù)。事實上,我現(xiàn)在懷疑這才是它們的
設(shè)計目的(至少是對構(gòu)造函數(shù)而言):更象是個異常過濾器而不是異常處理函數(shù)。我將繼續(xù)
研究下去,以發(fā)現(xiàn)這些規(guī)則后面的原理。
現(xiàn)在,至少,我們被迫使用一個不怎么直接的解決方法:
template <typename T>
class wrapper
{
public:
wrapper() throw()
: value_(NULL)
{
try
{
value_ = new T;
}
catch (...)
{
}
}
// ...
private:
T *value_;
// ...
};
被包容的對象,原來是在wrapper::wrapper()進入前構(gòu)造的,現(xiàn)在是在其函數(shù)體內(nèi)構(gòu)
造的了。這個變化可以讓我們使用普通的方法來捕獲異常而不用function try 塊了。
因為value_現(xiàn)在是個T *而不是T 對象了,get()和set()必須使用指針的語法了:
T get()
{
return *value_;
}
void set(T const &value)
{
*value_ = value;
}
14.4 Leak #1A:operator new
在構(gòu)造函數(shù)內(nèi)的try 塊中,語句
value_ = new T;
隱含地調(diào)用了operator new 來分配*value_的內(nèi)存。而這個operator new 函數(shù)可能拋異常。
幸好,我們的wrapper::wrapper()能同時捕獲T 的構(gòu)造函數(shù)和operator new 函數(shù)拋出
的異常,因此維持了接口安全。但,記住這個關(guān)鍵性的差異:
?? 如果T 的構(gòu)造函數(shù)拋了異常,operator delete 被隱含調(diào)用了來釋放分配的內(nèi)存。(對
于placement new,這取決于是否存在匹配的operator delete,我在part 8 和9 說過
了的。)
?? 如果operator new 拋了異常,operator delete 不會被隱含調(diào)用。
第二點本不該有什么問題:如果operator new 拋了異常,通常是因為內(nèi)存分配失敗,
operator delete 沒什么需要它去釋放的。但,如果operator new 成功分配了內(nèi)存但因為
其它原因而仍然拋了異常,它必須負(fù)責(zé)釋放內(nèi)存。換句話說,operator new 自己必須是行
為安全的。
(同樣的問題也發(fā)生在通過operator nwe[]創(chuàng)建數(shù)組時。)
14.5 Leak #1B:Destructor
想要wrapper 行為安全,我們需要它的析構(gòu)函數(shù)釋放new 出來的內(nèi)存:
~wrapper() throw()
{
delete value_;
}
這看起來很簡單,但請等一下說大話!delete value_調(diào)用*value_的析構(gòu)函數(shù),而這個
析構(gòu)函數(shù)可能拋異常。要實現(xiàn)~wrapper()的接口異常,我們必須加上try 塊:
~wrapper() throw()
{
try
{
delete value_;
}
catch (...)
{
}
}
但這還不夠。如果*value_的析構(gòu)函數(shù)拋了異常,operator delete 不會被調(diào)用了來釋
放*value_的內(nèi)存。我們需要加上行為安全:
~wrapper() throw()
{
try
{
delete value_;
}
catch (...)
{
operator delete(value_);
}
}
仍然沒結(jié)束。C++標(biāo)準(zhǔn)運行庫申明的operator delete 為
void operator delete(void *) throw();
它是不拋異常了,但自定義的operator delete 可沒說不拋。要想超級安全,我們應(yīng)該寫:
~wrapper() throw()
{
try
{
delete value_;
}
catch (...)
{
try
{
operator delete(value_);
}
catch (...)
{
}
}
}
但這還存在危險。語句
delete value_;
隱含調(diào)用了operator delete。如果它拋了異常,我們將進入catch 塊,一步步執(zhí)行下去并
再次調(diào)用同樣的operator delete!我們將程序連續(xù)暴露在同樣的異常下。這不會是個好程
序的。
最后,記?。簅perator delete 在被new 出對象的構(gòu)造函數(shù)拋異常時被隱含調(diào)用。如果
這個被隱含調(diào)用的operator delete 也拋了異常,程序?qū)⑻幱趦纱萎惓顟B(tài)并調(diào)用
terminate()。
原則:不要在一個可能在異常正被處理過程被調(diào)用的函數(shù)中拋異常。尤其是,不要從
下列情況下拋異常:
?? destructors
?? operator delete
?? operator delete[]
幾個小習(xí)題:用auto_ptr 代替value_,然后重寫wrapper 的構(gòu)造函數(shù),并決定其虛構(gòu)
函數(shù)的角色(如果需要的話),條件是必須保持異常安全。
14.6 題外話
我本準(zhǔn)備一次完成異常安全的。但現(xiàn)在是第二部分,并仍然有足夠的素材寫成第三部分
(我發(fā)誓那是最后的部分)。下次,我將討論get()和set()上的異常安全問題,和今天的內(nèi)
容同樣精彩。
回到目錄
15. 模板安全(續(xù))
在異常安全的第二部分,我講了在構(gòu)造函數(shù)和析構(gòu)函數(shù)中導(dǎo)致資源泄漏的問題。這次將
探索另外兩個問題。并且以推薦讀物列表結(jié)束。
15.1 Problem #2:get
上次,我定義X::get()為:
T get()
{
return *value_;
}
這個定義有點小小的不足。既然get()不改變wrapper 對象,我應(yīng)該將它申明為const
成員的:
T get() const
{
return *value_;
}
get()返回了一個T 的臨時對象。這個臨時對象通過T 的拷貝構(gòu)造函數(shù)根據(jù)*value_隱式
生成的,而這個構(gòu)造函數(shù)可能拋異常。要避開這點,我們應(yīng)該將get()修改為不返回任何東
西:
void get(T &value) const throw()
{
value = *value_;
}
現(xiàn)在,get()接受一個事先構(gòu)造好的T 對象的引用,并通過引用“返回”結(jié)果。因為get()
現(xiàn)在不調(diào)用T 的構(gòu)造函數(shù)了,它是異常安全的了。
真的嗎?
很不幸,答案是“no”。我們只是將一個問題換成了另外一個問題而已,因為語句
value = *value_;
實際上是
value.operator=(*value_);
而它可能拋異常。更完備的解決方法是
void get(T &value) const throw()
{
try
{
value = *value_;
}
catch (...)
{
}
}
現(xiàn)在,get()不會將異常漏出去了。
不過,工作還沒完成。在operator=給value 賦值時拋異常的話,value 將處于不確定
狀態(tài)。get()想要有最大程度的健壯接口的話,它必須兩者有其一:
?? value 根據(jù)*value_進行了完全設(shè)置,或
?? value 沒有被改變。
這兩條要將我們弄跳起來了:無論我們用什么方法來解決這個問題,我們都必須調(diào)用
operator=來設(shè)置value,而如果operator=拋了異常,value 將只被部分改變。
我們的這個強壯接口看起來美卻不實在。我們無法簡單地實現(xiàn)它,只能提供一個弱些的
承諾了:
?? value 根據(jù)*value_進行了完全設(shè)置,或
?? value 處于一個不確定的(錯誤)狀態(tài)。
但還有一個問題沒解決:讓調(diào)用者知道回傳的value 是否是“好的”。一個可能的解決
方法(也很諷刺的)是拋出一個異常。另外一個可能方法,也是我在這兒采用的方法是返回
一個錯誤碼。
修改后的get()是:
bool get(T &value) const throw()
{
bool error(false);
try
{
value = *value_;
}
catch (...)
{
error = true;
}
return error;
}
提供了一個較弱的承諾的這個新接口是安全的。它行為安全嗎?是的。wrapper 所擁有
的唯一資源是分配給*value_的內(nèi)存,而它是受保護的,即使operator=拋了異常。
符合最初的說明,get()有了一個健壯的異常安全承諾,即使T 沒有這個承諾。最終,
我們過于加強了get()的承諾(這取決于value),而應(yīng)該將它降低到T 的承諾層次。我們用
一個警告修正get()的承諾,基于我們不能控制或不能預(yù)知T 的狀態(tài)。In the end, we
over-committed get's guarantee (the determinism of value), and had to bring it down
to T's level. We amended get's contract with a caveat, based on conditions in T we
couldn't control or predict.
原則:程序的健壯性等于它最弱的承諾。盡可能提供最健壯的承諾,同時在行為和接口
上。
推論:如果你自己的接口的承諾比其他人的接口健壯,你通常必須將你的接口減弱到相
匹配的程度。
15.2 Problem #3:set
我們現(xiàn)在的X::set()的實現(xiàn)是:
void set(T const &value)
{
*value_ = value;
}
(和get()不同,set()確實修改wrapper 對象,所以不能申明為cosnt。)
語句
*value_ = value;
應(yīng)該看起來很熟悉:她只是前面Problem #2 中提到的語句
value = *value_;
的反序。注意到這個變化,Problem #3 的解決方案就和Problem #2 的一樣了:bool set(T
const &value) throw()
{
bool error(false);
try
{
*value = value_;
}
catch (...)
{
error = true;
}
return error;
}
和我們在get()中回傳value 遇到的問題一樣:如果operator=拋了異常,我們無法知
道*value_的狀態(tài)。我們對get()的承諾的警告在這兒同樣適用。
get()和set()現(xiàn)在有這同樣的操作但不同的用途:get()將當(dāng)前對象的值賦給另外一個
對象,而set()將另外一個對象的值賦給當(dāng)前對象。由于這種對稱性,我們可以將共同的代
碼放入一個assign()函數(shù):
static bool assign(T &to, T const &from) throw()
{
bool error(false);
try
{
to = from;
}
catch (...)
{
error = true;
}
return error;
}
使用了這個輔助函數(shù)后,get()和set()縮短為
bool get(T &value) const throw()
{
return assign(value, *value_);
}
bool set(T const &value) throw()
{
return assign(*value_, value);
}
15.3 最終版本
wrapper 的最終版本是
template <typename T>
class wrapper
{
public:
wrapper() throw()
: value_(NULL)
{
try
{
value_ = new T;
}
catch (...)
{
}
}
~wrapper() throw()
{
try
{
delete value_;
}
catch (...)
{
operator delete(value_);
}
}
bool get(T &value) const throw()
{
return assign(value, *value_);
}
bool set(T const &value) throw()
{
return assign(*value_, value);
}
private:
bool assign(T &to, T const &from) throw()
{
bool error(false);
try
{
to = from;
}
catch (...)
{
error = true;
}
return error;
}
T *value_;
wrapper(wrapper const &);
wrapper &operator=(wrapper const &);
};
(哇!52 行,原來只有20 行的!而且這還只是一個簡單的例子。)
注意,所有的異常處理函數(shù)只是吸收了那些異常而沒有做任何處理。雖然這使得
wrapper 異常安全,卻沒有紀(jì)錄下導(dǎo)致這些異常的原因。
我在Part13 中講的在構(gòu)造函數(shù)上的相沖突的原則在這兒同樣適用。異常安全是不夠的,
并且實際上是達(dá)不到預(yù)期目的的,如果它掩蓋了最初的異常狀態(tài)的話。同時,如果異常對象
在被捕獲前就弄死了程序的話,大部分的異常恢復(fù)方案都將落空。最后,良好的設(shè)計必須滿
足下兩個原則:
?? 通過異常對象的存在來注視異常狀態(tài),并適當(dāng)?shù)刈龀龇磻?yīng)。
?? 確保創(chuàng)造和傳播異常對象不會造成更大的破壞。(別讓治療行為比病本身更糟糕。)
15.4 其它說法
在過去3 部分中,我剖析了異常安全。我強烈建議你讀一下這些文章:
?? The first principles of C++ exception safety come from Tom Cargill's "Exception
Handling: A False Sense of Security," originally published in the November and
December 1994 issues of C++ Report. This article, more than any other, alerted
us to the true complexities and subtleties of C++ exception handling.
?? C++ Godfather Bjarne Stroustrup is writing an exception-safety Appendix for his
book The C++ Programming Language (Third Edition)
(http://www.research.att.com/~bs/3rd.html). Bjarne's offering a draft version
(http://www.research.att.com/~bs/3rd_safe0.html) of that chapter on the
Internet.
?? I tend to think of exception safety in terms of contracts and guarantees, ideas
formalized in Bertrand Meyer's "Design by Contract"
(http://www.eiffel.com/doc/manuals/technology/contract/page.html)
programming philosophy. Bertrand realizes this philosophy in both his seminal
tome Object-Oriented Software Construction
(http://www.eiffel.com/doc/oosc.html) and his programming language Eiffel
(http://www.eiffel.com/eiffel/page.html).
?? Herb Sutter has written the most thorough C++ exception-safety treatise I've
seen. He's published it as Items 8-19 of his new book Exceptional C++
(http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=0201615622). If
you've done time on Usenet's comp.lang.c++.moderated newsgroup, you've seen
Herb's Guru of the Week postings. Those postings inspired the bulk of his book.
Highly recommended.
?? Herb's book features a forward written by Scott Meyers. Scott covers exception
safety in Items 9-15 of his disturbingly popular collection More Effective C++
(http://www1.fatbrain.com/asp/bookinfo/bookinfo.asp?theisbn=020163371X). If
you don't have this book, you simply must acquire it; otherwise Scott's royalties
could dry up, and he'd have to get a real job like mine.
Scott(在他的Item14)認(rèn)為,不應(yīng)該將異常規(guī)格申明加到模板成員上,和我的正相反。
事實是無論用不用異常規(guī)格申明,總有一部分程序需要保護所有異常,以免程序自毀。Scott
公正地指出不正確的異常規(guī)格申明將導(dǎo)致std::unexpected――這正是他建議你避開的東
西;但,在本系列的Part11,我指出unexpected 比不可控的異常傳播要優(yōu)越。
最后要說的是,這兒不會只有一個唯一正確的答案的。我相信異常規(guī)格申明可以導(dǎo)致更
可預(yù)知和有限度的異常行為,即使是對于模板。我也得坦率地承認(rèn),在異常/模板混合體上
我也沒有足夠經(jīng)驗,尤其是對大系統(tǒng)。我估計還很少有人有這種經(jīng)驗,因為(就我所知)還
沒有哪個編譯器支持C++標(biāo)準(zhǔn)在異常和模板上的全部規(guī)定。
回到目錄
16. 指導(dǎo)方針
根據(jù)讀者們的建議,經(jīng)過反思,我部分修正在Part14 中申明的原則:
?? 只要可能,使用那些構(gòu)造函數(shù)不拋異常的基類和成員子對象。
?? 不要從你的構(gòu)造函數(shù)中拋出任何異常。
這次,我將思考讀者的意見,C++先知們的智慧,以及我自己的新的認(rèn)識和提高。然后
將它們轉(zhuǎn)化為指導(dǎo)方針來闡明和引申那些最初的原則。
(關(guān)鍵字說明:我用“子對象”或“被包容對象”來表示數(shù)組中元素、無名的基類、有
名的數(shù)據(jù)成員;用“包容對象”來表示數(shù)組、派生類對象或有數(shù)據(jù)成員的對象。)
16.1 C++的精髓
你可能認(rèn)為構(gòu)造函數(shù)在遇到錯誤時有職責(zé)拋異常以正確地阻止包容對象的構(gòu)造行為。
Herb Sutter 在一份私人信件中寫道:
一個對象的生命期始于構(gòu)造完成。
推論:一個對象當(dāng)它的構(gòu)造沒有完成時,它從來就沒存在過。
推論:通報構(gòu)造失敗的唯一方法是用異常來退出構(gòu)造函數(shù)。
我估計你正在做這種概念上就錯誤的事(“錯”是因為它不符合C++的精髓),而這也正
是做起來困難的原因。
“C++的精髓”是主要靠口頭傳授的C++神話。它是我們最初的法則,從ISO 標(biāo)準(zhǔn)和實
際中得出的公理。如果沒有存在過這樣的C++精髓的圣經(jīng),混亂將統(tǒng)治世界。Given that no
actual canon for the Spirit exists, confusion reigns over what is and is not within
the Spirit, even among presumed experts.
C 和C++的精髓之一是“trust the programmer”。如同我寫給Herb 的:
最終,我的“完美”觀點是:在錯誤的傳播過程中將異常映射為其它形式應(yīng)該是系統(tǒng)設(shè)
計人員選定的。這么做不總是最佳的,但應(yīng)該這么做。C++最強同時也是最弱的地方是你可
以偏離你實際上需要的首選方法。還有一些其它被語言許可的危險影行為,取決于你是否知
道你正在做什么。In the end, my "perfect" objective was to map exceptions to some
other form of error propagation should a designer choose to do so. Not that it was
always best to do so, but that it could be done. One of the simultaneous
strengths/weaknesses of C++ is that you can deviate from the preferred path if you
really need to. There are other dangerous behaviors the language tolerates, under
the assumption you know what you are doing.
C++標(biāo)準(zhǔn)經(jīng)常容忍甚至許可潛在的不安全行為,但不是在這個問題上。顯然,認(rèn)同程序
員的判斷力應(yīng)該服從于一個更高層次的目的(Apparently, the desire to allow programmer
discretion yields to a higher purpose)。Herb 在C++精髓的第二個表現(xiàn)形式上發(fā)現(xiàn)了這
個更高層次的目的:一個對象不是一個真正的對象(因此也是不可用的),除非它被完全構(gòu)
造(意味著它的所有要素也都被完全構(gòu)造了)。
看一下這個例子:
struct X
{
A a;
B b;
C c;
void f();
};
try
{
X x;
x.f();
}
catch (...)
{
}
這里,A、B 和C 是其它的類。假設(shè)x.a 和x.b 的構(gòu)造完成了,而x.c 的構(gòu)造過程中拋
了異常。如我們在前面幾部分中看到的,語言規(guī)則規(guī)定執(zhí)行這樣的序列:
?? x 的構(gòu)造函數(shù)拋了異常
?? x.b 的析構(gòu)函數(shù)被調(diào)用
?? x.a 的析構(gòu)函數(shù)被調(diào)用
?? 控制權(quán)交給異常處理函數(shù)
這個規(guī)則符合C++的精髓。因為x.c 沒有完成構(gòu)造,它從未成為一個對象。于是,x 也
從未成為一個對象,因為它的一個內(nèi)部成員(x.c)從沒存在過。因為沒有一個對象真的存
在過,所以也沒有哪個需要正式地析構(gòu)。
現(xiàn)在假設(shè)x 的構(gòu)造函數(shù)不知怎么控制住了最初的異常。在這種情況下,執(zhí)行序列將是:
?? x.f()被調(diào)用
?? x.c 的析構(gòu)函數(shù)被調(diào)用
?? x.b 的析構(gòu)函數(shù)被調(diào)用
?? x.a 的析構(gòu)函數(shù)被調(diào)用
?? x 的析構(gòu)函數(shù)被調(diào)用
?? 控制權(quán)跳過異常處理函數(shù)向下走
于是異常將會允許析構(gòu)那些從沒被完全構(gòu)造的對象(x.c 和x)。這將造成自相矛盾:一
個死亡的對象是從來都沒有產(chǎn)生過的。通過強迫構(gòu)造函數(shù)拋異常,語言構(gòu)造避免了這種矛盾。
16.2 C++的幽靈
前面表明一個對象當(dāng)且僅當(dāng)它的成員被完全構(gòu)造時才真的存在。但真的一個對象存在等
價于被完全構(gòu)造?尤其x.c 的構(gòu)造失敗“總是”如此惡劣到x 必須在真的在被產(chǎn)生前就死亡?
在C++語言有異常前,x 的定義過程必定成功,并且x.f()的調(diào)用將被執(zhí)行。代替拋異
常的方法,我們將調(diào)用一個狀態(tài)檢測函數(shù):
X x;
if (x.is_OK())
x.f();
或使用一個回傳狀態(tài)參數(shù):
bool is_OK;
X x(is_OK);
if (is_OK)
x.f();
在那個時候,我們不知何故在如x.c 這樣的子對象的構(gòu)造失敗時沒有強調(diào):這樣的對象
從沒真的存在過。那時的設(shè)計真的這么根本錯誤(而我們現(xiàn)在絕不允許的這樣行為了)? C++
的精髓真的在那時是不同的?或者我們生活在夢中,沒有想到過x 真的沒有成形、沒有存在
過?
公正地說,這個問題有點過份,因為C++語言現(xiàn)在和過去相比已不是同樣的語言。將老
的(異常支持以前)的C++當(dāng)作現(xiàn)在的C++如同將C 當(dāng)作C++。雖然它們有相同的語法,但
語意卻是不相同的。看一下:
struct X
{
X()
{
p = new T; // assume 'new' fails
}
void f();
};
X x;
x.f();
假設(shè)new 語句沒有成功分配一個T 對象。異常支持之前的編譯器(或禁止異常的現(xiàn)代編
譯器)下,new 返回NULL,x 的構(gòu)造函數(shù)和x.f()被調(diào)用。但在異常允許后,new 拋異常,x
構(gòu)造失敗,x.f()沒有被調(diào)用。同樣的代碼,非常不同的含意。
在過去,對象沒有自毀的能力,它們必須構(gòu)造,并且依賴我們來發(fā)現(xiàn)它的狀態(tài)。它們不
處理構(gòu)造失敗的子對象。并且,它們不調(diào)用標(biāo)準(zhǔn)運行庫中拋異常的庫函數(shù)。簡而言之,過去
的程序和現(xiàn)在的程序存在于不同的世界中。我們不能期望它們對同樣的錯誤總有同樣的反
應(yīng)。
16.3 這是你的最終答案嗎?
我現(xiàn)在相信C++標(biāo)準(zhǔn)的行為是正確的:構(gòu)造函數(shù)拋異常將析構(gòu)正在處理的對象及其包容
對象。我不知道C++標(biāo)準(zhǔn)委員會制訂這個行為的精確原因,但我猜想是:
?? 部分構(gòu)造的對象將導(dǎo)致一些微妙的錯誤,因為它的使用者對其的構(gòu)造程度的假設(shè)超過了
實際。同樣的類的不同對象將會有出乎意料的和不可預(yù)測的不同行為。
?? 編譯器需要額外的紀(jì)錄。當(dāng)一個部分構(gòu)造的對象消失時,編譯器要避免對它及它的部分
構(gòu)造的子對象調(diào)用析構(gòu)函數(shù)。
?? 對象被構(gòu)造和對象存在的等價關(guān)系將被打破,破壞了C++的精髓。
16.4 對對象的使用者的指導(dǎo)
異常是對象的接口的一部分。如果能夠,事先準(zhǔn)備好接口可能拋的異常集。如果一個接
口沒有提供異常規(guī)格申明,而且又不能從其它地方得知其異常行為,那么假設(shè)它可能在任何
時候拋任意的異常。
換句話說,準(zhǔn)備好捕獲或至少要過濾所有可能的異常。不要讓任何異常在沒有被預(yù)料到
的情況下進入或離開你的代碼;即使你只是簡單地傳遞或重新拋出異常,也必須是經(jīng)過認(rèn)真
選擇的。
16.5 構(gòu)造函數(shù)拋異常
準(zhǔn)備好所有子對象的構(gòu)造函數(shù)可能拋的異常的異常集,并在你的構(gòu)造函數(shù)中捕獲它們。
如:
struct A
{
A() throw(char, int);
};
struct B
{
B() throw(int);
};
struct C
{
C() throw(long);
};
struct X
{
A a;
B b;
C c;
X();
};
子對象構(gòu)造函數(shù)的異常集是{char,int,long}。它就是X 的構(gòu)造函數(shù)遭遇的可能異常。
如果X 的構(gòu)造函數(shù)未經(jīng)過濾就傳遞這些異常,它的異常規(guī)格申明將是
X() throw(char, int, long);
但使用function try 塊,構(gòu)造函數(shù)可以將這些異常映射為其它類型:
X() throw(unsigned)
try
{
// ... X::X body
}
catch (...)
{
// map caught sub-object exceptions to another type
throw 1U; // type unsigned
}
如同前面的部分所寫,用戶的構(gòu)造函數(shù)不能阻止子對象的異常傳播出去,但能控制傳遞
出去的類型,通過將進入的異常映射為受控的傳出類型(這兒是unsigned)。
16.6 構(gòu)造函數(shù)不拋異常
如果沒有子對象的構(gòu)造函數(shù)拋異常,其異常集是空,表明包容對象的構(gòu)造函數(shù)不會遇到
異常。唯一能確定你的構(gòu)造函數(shù)不拋異常的辦法是只包容不拋異常的子對象。
如果必須包容一個可能拋異常的子對象,但仍然不想從你自己的構(gòu)造函數(shù)中拋出異常,
考慮使用被叫做Handle Class 或Pimpl 的方法(“Pimpl”個雙關(guān)語:pImpl 或“pointer to
implementation”)。長久以來被用作減短編譯時間的技巧,它也提高異常安全性。
回到前面的例子:
class X
{
public:
X();
// ...other X members
private:
A a;
B b;
C c;
};
根據(jù)這種方法,必須將X 分割為兩個獨立的部分。第一部分是被X 的用戶引用的“公有”
頭文件:
struct X_implementation;
class X
{
public:
X() throw();
// ...other X members
private:
struct X_implementation *implementation;
};
而第二部分是私有實現(xiàn)
struct X_implementation
{
A a;
B b;
C c;
};
X::X() throw()
{
try
{
implementation = new X_implementation;
}
catch (...)
{
// ... Exception handled, but not implicitly rethrown.
}
}
// ...other X members
X 的構(gòu)造函數(shù)捕獲了構(gòu)造*implementation 過程(也就是構(gòu)造a、b 和c 的過程)中的所
有異常。更進一層,如果數(shù)據(jù)成員變了,X 的用戶不需要重新編譯,因為X 的頭文件沒有變
化。
(反面問題:如果X::X 捕獲了一個異常,*implementation 及至少子對象a/b/c 中的
一個沒有完全構(gòu)造。但是,包容類X 的對象作為一個有效實體延續(xù)了生命期。這個X 的部分
構(gòu)造的對象的存在違背C++精髓嗎?)
許多C++的指導(dǎo)手冊討論這個方法,所以我不在這兒詳述了。一個極其詳細(xì)的討論出現(xiàn)
在Herb Sutter 的著作《Exceptional C++》的Items26-30 上。
16.7 對對象提供者的指導(dǎo)
不要將異常體系等同于一種錯誤處理體系,認(rèn)為它和返回錯誤碼或設(shè)置全局變量處在同
一層次上。異常根本性地改變了它周圍的代碼的結(jié)構(gòu)和意義。它們臨時地改變了程序的運行
期語意,跳過了一些通常都運行的代碼,并激活其它從沒被運行的代碼。它們強迫你的程序
回應(yīng)和處理可導(dǎo)致程序死亡的錯誤狀態(tài)。
因此,異常的特性和簡單的錯誤處理大不相同。如果你不希望這些特性,或不理解這些
特性,或不想將這些特性寫入文檔,那么不要拋異常,使用其它的錯誤處理體系。
如果決定拋異常,必須明白全部的因果關(guān)系。明白你的決定對使用你的代碼的人有巨大
的潛在影響。你的異常是你的接口的一部分;你必須在文檔中寫入你的接口將拋什么異常,
什么時候拋,以及為什么拋。并將這文檔在異常規(guī)格申明出注釋出來。
16.8 構(gòu)造函數(shù)拋異常
如果你的構(gòu)造函數(shù)拋異常,或你(直接地或間接地)包容的某個子對象拋異常,包容你
的對象的用戶對象也將拋異常并因此構(gòu)造失敗。這就是重用你的代碼的用戶的代價。要確保
這個代價值得。
你沒有被強迫要在構(gòu)造函數(shù)里拋異常,老的方法仍然有效的。當(dāng)你的構(gòu)造函數(shù)遇到錯誤
時,你必須判斷這些錯誤是致命的還是稍有影響。拋出一個構(gòu)造異常傳遞了一個強烈的信息:
這個對象被破壞且無法修補。返回一個構(gòu)造狀態(tài)碼表明一個不同信息:這個對象被破壞但還
具有功能。
不拋異常只是因為它是一個時髦的方法:在一個對象真的不能或不該生存時,推遲其自
毀。
16.9 過職
別讓你的接口過職。如果知道你的接口的精確異常集,將它在異常規(guī)格申明中列舉出來。
否則,不提供異常規(guī)格申明。沒有異常規(guī)格申明比撒謊的異常規(guī)格申明好,因為它不會欺騙
用戶。
這條規(guī)則的可能例外是:模板異常。如前三部分所寫,模板的編寫者通常不知道可能拋
出的異常。如果你的模板不提供異常規(guī)格申明,用戶將降低安全感和信心。如果你的模板有
異常規(guī)格申明你必須:
?? 要么使用前面看過的異常安全的技巧來確保異常規(guī)格申明是精確的
?? 要么在文檔中寫下你的模板只接受有確定特性的參數(shù)類型,并警告其它類型將導(dǎo)致失控
(with the caveat that other types may induce interface-contract violations
beyond your control)。
16.10 必要vs 充分
不要人為增加你的類的復(fù)雜度,只是為了適應(yīng)所有可能的需求。不是所有對象都會被重
用的。如pet Becker 寫給我的:
現(xiàn)在的程序員花了太多的時間來應(yīng)付可能發(fā)生的事情,而他們本應(yīng)該簡單地拒絕的。如
果有一個拋異常的好理由的話,大膽地拋異常,并寫入文檔,不要創(chuàng)造一些精巧的方法來避
免拋這些異常。增加的復(fù)雜度可能導(dǎo)致維護上的惡夢,超過了錯誤使用受限版本時遇到的痛
苦。
Pete 的說法對析構(gòu)函數(shù)也同樣有用??匆幌逻@條原則(從Part14 引用過來的):
不要在析構(gòu)函數(shù)中拋異常。
一般來說,符合這條原則比違背它好。但,有時不是這樣的:
?? 如果你準(zhǔn)備讓其他人包容你的對象,或至少不禁止別人包容你的對象,那么別在析構(gòu)函
數(shù)中拋異常。
?? 如果你真的有理由拋異常,并且知道它違背了安全策略,那么大膽地拋異常,在文檔中
寫入原因。
就如同在設(shè)計的時候必須考慮異常處理,也必須考慮重用。在析構(gòu)函數(shù)上申明throw()
是成為一個好的子對象的必要條件,但遠(yuǎn)不充分。你必須前瞻性地考慮你的代碼將遇到什么
上下文,它將容忍什么、將反抗什么。如果增加了設(shè)計的復(fù)雜度,確保這些復(fù)雜度是策略的
一部分,而不是脆弱的“以防萬一”的保險單。
16.11 感謝
(略)
除了一些零星的東西,我已經(jīng)完成了異常安全的主題!實際上我也幾乎完成了異常的專
題。下次時間暫停,在三月中將討論很久前承諾的C++異常和Visual C++ SEH 的混合使用。
回到目錄
17. C++異常和Visual C++ SEH 的混合使用
我在Part2 介紹了Structured Exception Handling(簡稱SEH)。在那時我就說過,SEH
是window 及其平臺上的編譯器專有的。它不是定義在ISO C++標(biāo)準(zhǔn)中的,使用它的程序?qū)?br>不能跨編譯器移植。因為我注重于標(biāo)準(zhǔn)兼容和可移植性,所以我對將windows 專有的SEH
映射為ISO 標(biāo)準(zhǔn)C++的exception handing(簡稱EH)很感興趣。
同時,我不是SEH 的專家。對它的了解絕大部分來自于本專欄前面的研究。當(dāng)我考慮混
合使用SEH 與EH 時,我猜想解決方法應(yīng)該是困難的和不是顯而易見的。這是它花了我兩個
星期的原因:我預(yù)料到需要額外的時間來研究和試驗。
很高興,我完全錯了。我不知道的是Visual C++運行期庫直接支持了絕大部分我所想
要的東西。不用創(chuàng)造新的方法了,我可以展示你Visual C++已經(jīng)支持了的東西,以及改造
為所需要的東西的方法?;谶@個目的,我將研究同一個例子的四個不同版本。
17.1 Version 1:定義一個轉(zhuǎn)換函數(shù)
捆綁SEH 和EH 的方法分兩步:
?? 一個用戶自定義的轉(zhuǎn)換函數(shù)來捕獲SEH 的異常并將它映射為C++的異常。
?? 一個Visual C++運行期庫函數(shù)來安裝這個轉(zhuǎn)換函數(shù)
用戶自定義的轉(zhuǎn)換函數(shù)必要有如下形式:
void my_translator(unsigned code, EXCEPTION_POINTERS *info);
轉(zhuǎn)換函數(shù)接受一個SEH 異常(通過給定的異常code 和info 來定義的)。然后拋出一個
C++異常,以此將傳入的SEH 異常映射為向外傳的C++異常。這個C++異常將出現(xiàn)在原來的
SEH 異常發(fā)生點上并向外傳播。
這個機制非常象std::set_terminate()和std::set_unexpected()。要安裝轉(zhuǎn)換函數(shù),
要調(diào)用Visual C++庫函數(shù)_set_se_translator()。這個函數(shù)申明在頭文件eh.h 中:
typedef void (*_se_translator_function)(unsigned, EXCEPTION_POINTERS *);
_se_translator_function _set_se_translator(_se_translator_function);
它接受一個指向新轉(zhuǎn)換函數(shù)的指針,返回上次安裝的指針。一旦安裝了一個轉(zhuǎn)換函數(shù),
前一次的就丟失了;任何時候只有一個轉(zhuǎn)換函數(shù)有效。(在多線程程序中,每個線程有一個
獨立的轉(zhuǎn)換函數(shù)。)
如果還沒安裝過轉(zhuǎn)換函數(shù),第一次調(diào)用_set_se_translator()返回值可能是(也可能不
是)NULL。也就是說,不能不分青紅皂白就通過其返回的指針調(diào)用函數(shù)。很有趣的,如果返
回值是NULL,而你又通過此NULL 調(diào)用函數(shù),將產(chǎn)生一個SEH 異常,并且進入你剛剛安裝的
轉(zhuǎn)換函數(shù)。
一個簡單的例子:
#include <iostream>
using namespace std;
int main()
{
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (unsigned exception)
{
cout << "caught C++ exception " << hex << exception << endl;
}
return 0;
}
運行它的話,這個控制臺程序?qū)?dǎo)致如此一個windows messagebox:
它是由于一個未被捕獲的SEH 異常傳遞到程序外面造成的。
現(xiàn)在,增加一個異常轉(zhuǎn)換函數(shù),并將Visual C++運行庫設(shè)為使用這個轉(zhuǎn)換函數(shù):
#include <iostream>
using namespace std;
#include "windows.h"
static void my_translator(unsigned code, EXCEPTION_POINTERS *)
{
throw code;
}
int main()
{
_set_se_translator(my_translator);
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (unsigned exception)
{
cout << "caught C++ exception " << hex << exception << endl;
}
return 0;
}
再運行程序?,F(xiàn)在將看到:
caught C++ exception c0000005
my_translator()截獲了SEH 異常,并轉(zhuǎn)換為C++異常,其類型為unsigned,內(nèi)容為SEH
異常碼(本例中為C0000005h,它是一個非法讀取錯誤)。因為這個C++異常出現(xiàn)在原來的
SEH 異常發(fā)生點,也就說在try 塊中,所以被try 塊的異常處理函數(shù)捕獲了。
17.2 Version 2:定義一個轉(zhuǎn)換對象
上面的例子非常簡單,將每個SEH 異常轉(zhuǎn)換為一個unsigned 值。實際上,你可能需要
一個比較復(fù)雜的異常對象:
#include <iostream>
using namespace std;
//#include "windows.h"
#include "structured_exception.h"
/*static void my_translator(unsigned code, EXCEPTION_POINTERS *)
{
throw code;
}*/
int main()
{
//_set_se_translator(my_translator);
structured_exception::install();
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (structured_exception const &exception)
{
cout << "caught C++ exception " << hex << exception.what()
<< " thrown from " << exception.where() << endl;
}
return 0;
}
這個例子拋出了一個用戶自定義類型(structured_exception)的C++異常。為了讓這
個例子更具實際意義,也更方便閱讀,我將structured_exception 的申明放到了頭文件
structured_exception.h 中:
#if !defined INC_structured_exception_
#define INC_structured_exception_
#include "windows.h"
class structured_exception
{
public:
structured_exception(EXCEPTION_POINTERS const &) throw();
static void install() throw();
unsigned what() const throw();
void const *where() const throw();
private:
void const *address_;
unsigned code_;
};
#endif // !defined INC_structured_exception_
其實現(xiàn)文件為:
#include "structured_exception.h"
#include "eh.h"
//
// ::
//
static void my_translator(unsigned, EXCEPTION_POINTERS *info)
{
throw structured_exception(*info);
}
//
// structured_exception::
//
structured_exception::structured_exception
(EXCEPTION_POINTERS const &info) throw()
{
EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
address_ = exception.ExceptionAddress;
code_ = exception.ExceptionCode;
}
void structured_exception::install() throw()
{
_set_se_translator(my_translator);
}
unsigned structured_exception::what() const throw()
{
return code_;
}
void const *structured_exception::where() const throw()
{
return address_;
}
這些函數(shù)的意義是:
?? my_translator()是異常轉(zhuǎn)換函數(shù)。我把它從main 文件中移到這兒。于是,main 文件
不再需要包含windows.h 了。
?? install()將運行器庫的全局轉(zhuǎn)換函數(shù)設(shè)置為my_translator()。
?? structured_exception 的構(gòu)造函數(shù)接收并解析SEH 異常的信息。
?? what()返回SEH 異常的異常碼。
?? where()返回SEH 異常發(fā)生的地點。注意,where()的返回類型是void const *,雖然
C++標(biāo)準(zhǔn)不同意將代碼地址轉(zhuǎn)換為void 指針。我只是重復(fù)了Micorsoft 的用法,因為
Visual C++運行庫將地址存在了SEH 異常的EXCEPTION_RECORD 的一個void *成員中了。
編譯并鏈接這三個文件。運行結(jié)果是:
caught C++ exception c0000005 thrown from 0040181D
(其中的代碼地址值在你的系統(tǒng)上可能有所不同。)
17.3 Version 3:模仿C++標(biāo)準(zhǔn)運行庫
在my_translator()中,所有的SEH 異常映射為同樣的structured_exception 類型。
這使得異常容易被捕獲,因為它們匹配于我們的唯一的異常處理函數(shù):
catch (structured_exception const &exception)
雖然捕獲了異常,但我們沒有辦法事先知道異常的類型。唯一能做的是運行期查詢,調(diào)
用這個異常的what()成員:
catch (structured_exception const &exception)
{
switch (exception.what())
{
case EXCEPTION_ACCESS_VIOLATION:
// ...
case EXCEPTION_INT_DIVIDE_BY_ZERO:
// ...
case EXCEPTION_STACK_OVERFLOW:
// ...
// ...
}
這樣的查詢需要windows.h 中的信息,以知道最初的SEH 異常碼的含意。這樣的需求違
背了structured_exception 的抽象原則。此外,switch 語句也經(jīng)常違背了多態(tài)的原則。從
用戶代碼的角度看,你通常應(yīng)該用繼承和模板來實現(xiàn)它。
C++標(biāo)準(zhǔn)運行庫在這方面提供了一些指導(dǎo)。如我在Part3 中勾畫的,頭文件<stdexcept>
定義了一個異常類層次,std::exception 是根結(jié)點。這個根類定義了虛成員what(),它返
回一個編譯器自定義的NTBS(C++標(biāo)準(zhǔn)中是“以NULL 結(jié)束的字符串”)。每個繼承類指定自
己的what()的返回值。雖然C++標(biāo)準(zhǔn)沒有規(guī)定這些值的內(nèi)容,但我相信標(biāo)準(zhǔn)委員會打算用這
個字符串來描述異常的類型或含意的。
根據(jù)這種精神,standard_exception 的申明是:
#if !defined INC_structured_exception_
#define INC_structured_exception_
#include "eh.h"
#include "windows.h"
class structured_exception
{
public:
structured_exception(EXCEPTION_POINTERS const &) throw();
static void install() throw();
virtual char const *what() const throw();
void const *where() const throw();
private:
void const *address_;
//unsigned code_;
};
class access_violation : public structured_exception
{
public:
access_violation(EXCEPTION_POINTERS const &) throw();
virtual char const *what() const throw();
};
class divide_by_zero : public structured_exception
{
public:
divide_by_zero(EXCEPTION_POINTERS const &) throw();
virtual char const *what() const throw();
};
#endif // !defined INC_structured_exception_
實現(xiàn)是:
#include <exception>
using namespace std;
#include "structured_exception.h"
#include "windows.h"
//
// ::
//
static void my_translator(unsigned code, EXCEPTION_POINTERS *info)
{
switch (code)
{
case EXCEPTION_ACCESS_VIOLATION:
throw access_violation(*info);
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
throw divide_by_zero(*info);
break;
default:
throw structured_exception(*info);
break;
}
}
//
// structured_exception::
//
structured_exception::structured_exception
(EXCEPTION_POINTERS const &info) throw()
{
EXCEPTION_RECORD const &exception = *(info.ExceptionRecord);
address_ = exception.ExceptionAddress;
//code_ = exception.ExceptionCode;
}
void structured_exception::install() throw()
{
_set_se_translator(my_translator);
}
char const *structured_exception::what() const throw()
{
return "unspecified Structured Exception";
}
void const *structured_exception::where() const throw()
{
return address_;
}
//
// access_violation::
//
access_violation::access_violation
(EXCEPTION_POINTERS const &info) throw()
: structured_exception(info)
{
}
char const *access_violation::what() const throw()
{
return "access violation";
}
//
// divide_by_zero::
//
divide_by_zero::divide_by_zero
(EXCEPTION_POINTERS const &info) throw()
: structured_exception(info)
{
}
char const *divide_by_zero::what() const throw()
{
return "divide by zero";
}
注意:
?? 那些本來在用戶的異常處理函數(shù)中的switch 語句,現(xiàn)在移到了my_translator()中。
不再是將所有SEH 異常映射為單個值(如version 1 中)或單個類型的對象(version 2),
現(xiàn)在的my_translator()將它們映射為多個類型的對象(取決于運行時的實際環(huán)境)。
?? structured_exception 成為了一個基類。我沒有讓它成為純虛類,這是跟從了C++標(biāo)準(zhǔn)
運行庫的引導(dǎo)(std::exception 是個實體類)。
?? 我沒有定義任何析構(gòu)函數(shù),因為編譯器隱含提供的的析構(gòu)函數(shù)對這些簡單類足夠了。如
果我定義了析構(gòu)函數(shù),它們將需要定義為virtual。
?? what()現(xiàn)在返回了一個用戶友好的文本,取代了原來的SEH 異常碼。
?? 因為我不再測試和顯示這些代碼, 我去掉了數(shù)據(jù)成員code_ 。這使得
structured_exception 對象的大小減小了。(別太高興:節(jié)省的空間又被新增的vptr
指針抵銷了,因為有了虛函數(shù)。)
?? 因為模板方式更好,你應(yīng)該放棄這種繼承模式的。我將它留給你作為習(xí)題。
試一下新的方案,將main 文件改為:
#include <iostream>
using namespace std;
#include "structured_exception.h"
int main()
{
structured_exception::install();
//
// discriminate exception by dynamic type
//
try
{
*(int *) 0 = 0; // generate Structured Exception
}
catch (structured_exception const &exception)
{
cout << "caught " << exception.what() << endl;
}
//
// discriminate exception by static type
//
try
{
static volatile int i = 0;
i = 1 / i; // generate Structured Exception
}
catch (access_violation const &)
{
cout << "caught access violation" << endl;
}
catch (divide_by_zero const &)
{
cout << "caught divide by zero" << endl;
}
catch (structured_exception const &)
{
cout << "caught unspecified Structured Exception" << endl;
}
return 0;
}
再次運行,結(jié)果是:
caught access violation
caught divide by zero
17.4 Version 4:匹配于C++標(biāo)準(zhǔn)運行庫
我們所有的standard_exception 繼承類都提供公有的成員
virtual char const *what() const;
來識別異常的動態(tài)類型。我不是隨便選取的函數(shù)名:所有的C++標(biāo)準(zhǔn)運行庫中的
std::exception 繼承類為同樣的目的提供了同樣的公有成員。并且,what()是每個繼承類
的唯一的多態(tài)函數(shù)。
你可能已經(jīng)注意到:
#include <exception>
class structured_exception : public std::exception
{
public:
structured_exception(EXCEPTION_POINTERS const &info) throw();
static void install() throw();
virtual char const *what() const throw();
void const *where() const throw();
private:
void const *address_;
};
因為structured_exception 現(xiàn)在也是一個std:exception,我們可以用一個異常處理
函數(shù)來同時捕獲這個異常族:
catch (std::exception const &exception)
并且用同樣的多態(tài)函數(shù)來獲取異常的類型:
catch (std::exception const &exception)
{
cout << "caught " << exception.what();
}
用這樣的方案,SEH 異常能夠表現(xiàn)得與標(biāo)準(zhǔn)C++的固有行為一致。同時,我們?nèi)匀荒軌?br>特殊對待structured_exceptions 并訪問它的特殊成員:
catch (structured_exception const &exception)
{
cout << "caught Structured Exception from " << exception.where();
}
當(dāng)然,如果你想放棄沒有出現(xiàn)在std::exception 繼承體系中的類成員,如where(),
你完全可以不使用基類structured_exception,而是直接從std::exception 繼承出
access_violation 等類。例如:一個divide-by-zero 異常表示了一個程序值域控制錯誤,
也就是說是個邏輯錯誤。你所以想直接從std::logic_error 甚至是std::out_of_range 派
生devide_by_zero 類。
我建議你看一下C++標(biāo)準(zhǔn)subclause 19.1 (“Exception classes”)以更好地理解C++
標(biāo)準(zhǔn)運行庫的異常繼承體系,以及如何更好地將你的自定義異常熔入此繼承體系。
17.5 總結(jié)束
(略)
回到目錄
 
本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊舉報。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
VC 運行時庫中的 new/delete 使用
理解C++中new背后的行為
一個跨平臺的 C 內(nèi)存泄漏檢測器
智能指針實現(xiàn)
newnewnew(transfered) - fleeting_ash的日志 - 網(wǎng)易博...
C++的錯誤和異常處理分析
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點擊這里聯(lián)系客服!

聯(lián)系客服