青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

隨筆-90  評論-947  文章-0  trackbacks-0

原文:http://blog.csdn.net/pongba/archive/2007/10/08/1815742.aspx

By 劉未鵬(pongba)
C++的羅浮宮(http://blog.csdn.net/pongba)
TopLanguage(http://groups.google.com/group/pongba)

引言

錯誤處理(Error-Handling)這個重要議題從1997年(也許更早)到2004年左右一直是一個被廣泛爭論的話題,曾在新聞組上、博客上、論壇上引發(fā)口水無數(shù)(不亞于語言之爭),Bjarne Stroustrup、James Gosling、Anders Hejlsberg、Bruce Eckel、Joel Spolsky、Herb Sutter、Andrei Alexandrescu、Brad Abrams、Raymond Chen、David Abrahams…,各路神仙紛紛出動,好不熱鬧:-)

如今爭論雖然已經(jīng)基本結(jié)束并結(jié)果;只不過結(jié)論散落在大量文獻當中,且新舊文獻陳雜,如果隨便翻看其中的幾篇乃至幾十篇的話都難免管中窺豹。就連Gosling本人寫的《The Java Programming Language》中也語焉不詳。所以,寫這篇文章的目的便是要對這個問題提供一個整體視圖,相信我,這是個有趣的話題:-)

為什么要錯誤處理?

這是關(guān)于錯誤處理的問題里面最簡單的一個。答案也很簡單:現(xiàn)實世界是不完美的,意料之外的事情時有發(fā)生。

一個現(xiàn)實項目不像在學校里面完成大作業(yè),只要考慮該完成的功能,走happy path(也叫One True Path)即可,忽略任何可能出錯的因素(呃.. 你會說,怎么會出錯呢?配置文件肯定在那,矩陣文件里面肯定含的是有效數(shù)字.. 因為所有的環(huán)境因素都在你的控制之下。就算出現(xiàn)什么不測,比如運行到一半被人拔了網(wǎng)線,那就讓程序崩潰好了,再雙擊一下不就行了嘛)。

然而現(xiàn)實世界的軟件就必須考慮錯誤處理了。如果一個錯誤是能夠恢復(fù)的,要盡量恢復(fù)。如果是不能恢復(fù)的,要妥善的退出模塊,保護用戶數(shù)據(jù),清理資源。如果有必要的話應(yīng)該記錄日志,或重啟模塊等等。

簡而言之,錯誤處理的最主要目的是為了構(gòu)造健壯系統(tǒng)。

什么時候做錯誤處理?(或者:什么是“錯誤”?)

錯誤,很簡單,就是不正確的事情。也就是不該發(fā)生的事情。有一個很好的辦法可以找出哪些情況是錯誤。首先就當自己是在一個完美環(huán)境下編程的,一切precondition都滿足:文件存在那里,文件沒被破壞,網(wǎng)絡(luò)永遠完好,數(shù)據(jù)包永遠完整,程序員永遠不會拿腦袋去打蒼蠅,等等… 這種情況下編寫的程序也被稱為happy path(或One True Path)。

剩下的所有情況都可以看作是錯誤。即“不應(yīng)該”發(fā)生的情況,不在算計之內(nèi)的情況,或者預(yù)料之外的情況,whatever。

簡而言之,什么錯誤呢?調(diào)用方違反被調(diào)用函數(shù)的precondition、或一個函數(shù)無法維持其理應(yīng)維持的invariants、或一個函數(shù)無法滿足它所調(diào)用的其它函數(shù)的precondition、或一個函數(shù)無法保證其退出時的postcondition;以上所有情況都屬于錯誤。(詳見《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》第70章,或《Object Oriented Software Construction, 2nd edition》第11、12章)

例如文件找不到(通常意味著一個錯誤)、配置文件語法錯誤、將一個值賦給一個總應(yīng)該是正值的變量、文件存在但由于訪問限制而不能打開,或打開不能寫、網(wǎng)絡(luò)傳輸錯誤、網(wǎng)絡(luò)中斷、數(shù)據(jù)庫連接錯誤、參數(shù)無效等。

不過話說回來,現(xiàn)實世界中,錯誤與非錯誤之間的界定其實是很模糊的。例如文件缺失,可能大多數(shù)情況下都意味著一個錯誤(影響程序正常執(zhí)行并得出正常結(jié)果),然而有的情況下也可能根本就不是錯誤(或者至少是可恢復(fù)的錯誤),比如你的街機模擬器的配置文件缺失了,一般程序只要創(chuàng)建一個缺省的即可。

因此,關(guān)于把哪些情況界定為錯誤,具體的答案幾乎總是視具體情況而定的。但話雖如此,仍然還是有一些一般性的原則,如下:

哪些情況不屬于錯誤?

1.      控制程序流的返回值不是錯誤:如果一個情況經(jīng)常發(fā)生,往往意味著它是用來控制程序流程的,應(yīng)該用status-code返回(注意,不同于error-code),比如經(jīng)典的while(cin >> i)。讀入整型失敗是很常見的情況,而且,這里的“讀入整型失敗”其實真正的含義是“流的下一個字段不是整型”,后者很明確地不代表一個錯誤;再比如在一個字符串中尋找一個子串,如果找不到該子串,也不算錯誤。這類控制程序流的返回值都有一個共同的特點,即我們都一定會利用它們的返回值來編寫if-else,循環(huán)等控制結(jié)構(gòu),如:

  if(foo(…)) { … }
  else { … }

  while(foo(…)) { … }

這里再摘兩個相應(yīng)的具體例子,一個來自Gosling的《The Java Programming Language》,是關(guān)于stream的。

使用status-code:

  while ((token = stream.next()) != Stream.END)
      process(token);
  stream.close();

使用exception:

  try {
    for(;;) {
      process(stream.next());
    }
  } catch (StreamEndException e) {
    stream.close();
  }

高下立判。

另一個例子來自TC++PL(Well, not exactly):

  size_t index;
  try {
    index = find(str, sub_str);
    … // case 1
  } catch (ElementNotFoundException& e) {
    … // case 2
  }

使用status-code:

  int index = find(str, sub_str)
  if(index != -1) {
    … // case 1
  } else {
    … // case 2
  }

以上這類情況的特點是,返回值本身也是程序主邏輯(happy path)的一部分,返回的兩種或幾種可能性,都是完全正常的、預(yù)料之中的。

1’. 另一方面,還有一種情況與此有微妙的區(qū)別,即“可恢復(fù)錯誤”。可恢復(fù)錯誤與上面的情況的區(qū)別在于它雖說也是預(yù)料之中的,但它一旦發(fā)生程序往往就會轉(zhuǎn)入一個錯誤恢復(fù)子過程,后者會盡可能恢復(fù)程序主干執(zhí)行所需要的某些條件,恢復(fù)成功程序則再次轉(zhuǎn)入主干執(zhí)行,而一旦恢復(fù)失敗的話就真的成了一個貨真價實的讓人只能干瞪眼的錯誤了。比如C++里面的operator new如果失敗的話會嘗試調(diào)用一個可自定義的錯誤恢復(fù)子過程,當然,后者并非總能成功將程序恢復(fù)過來。除了轉(zhuǎn)入一個錯誤恢復(fù)子過程之外,另一種可能性是程序會degenerate入一條second-class的支流,后者也許能完成某些預(yù)期的功能,但卻是“不完美”地完成的。

這類錯誤如何處理后面會討論。

2.      編程bug不是錯誤。屬于同一個人維護的代碼,或者同一個小組維護的代碼,如果里面出現(xiàn)bug,使得一個函數(shù)的precondition得不到滿足,那么不應(yīng)該視為錯誤。而應(yīng)該用assert來對付。因為編程bug發(fā)生時,你不會希望棧回滾,而是希望程序在assert失敗點上直接中斷,調(diào)用調(diào)試程序,查看中斷點的程序狀態(tài),從而解決代碼中的bug。

關(guān)于這一點,需要尤其注意的是,它的前提是:必須要是同一個人或小組維護的代碼。同一個小組內(nèi)可以保證查看到源代碼,進行debug。如果調(diào)用方和被調(diào)用方不屬于同一負責人,則不能滿足precondition的話就應(yīng)該拋出異常。總之記住一個精神:assert是用來輔助debug的(assert的另一個作用是文檔,描述程序在特定點上的狀態(tài),即便assert被關(guān)閉,這個描述功能也依然很重要)。

注意,有時候,為了效率原因,也會在第三方庫里面使用assert而不用異常來報告違反precondition。比如strcpy,std::vector的operator[]。

3.      頻繁出現(xiàn)的不是錯誤。頻繁出現(xiàn)的情況有兩種可能,一是你的程序問題大了(不然怎么總是出錯呢?)。二是出現(xiàn)的根本不是錯誤,而是屬于程序的正常流程。后者應(yīng)該改用status-code。

插曲:異常(exception)vs錯誤代碼(error-code)

異常相較于錯誤代碼的優(yōu)勢太多了,以下是一個(不完全)列表。

異常與錯誤代碼的本質(zhì)區(qū)別之一——異常會自動往上層棧傳播:一旦異常被拋出,執(zhí)行流就立即中止,取而代之的是自動的stack-unwinding操作,直到找到一個適當?shù)腸atch子句。

相較之下,使用error-code的話,要將下層調(diào)用發(fā)生的錯誤傳播到上層去,就必須手動檢查每個調(diào)用邊界,任何錯誤,都必須手動轉(zhuǎn)發(fā)(返回)給上層,稍有遺漏,就會帶著錯誤的狀態(tài)繼續(xù)往下執(zhí)行,從而在下游不知道離了多遠的地方最終引爆程序。這來了以下幾個問題:

1.      麻煩。每一個可能返回錯誤代碼的調(diào)用邊界都需要檢查,不管你實際上對不對返回的錯誤作響應(yīng),因為即便你自己不解決返回的錯誤,也要把它傳播到上層去好讓上層解決。

2.      不優(yōu)雅且不可伸縮(scalability)的代碼(錯誤處理代碼跟One True Path(也叫happy path)攪和在一起)。關(guān)于這一條普遍的論述都不夠明確,比如有人可能會反駁說,那錯誤反正是要檢查的,用異常難道就不需要捕獲異常了嗎?當然是需要的,但關(guān)鍵是,有時候我們不一定會在異常發(fā)生的立即點上捕獲并處理異常。這時候,異常的優(yōu)勢就顯現(xiàn)出來了,比如:

  void foo()
  {
    try {
      op1;
      op2;
      …
      opN;
    } catch (…) {
      … // log
      … // clean up
      throw;
    }
  }

如果用error-code的話:

  int foo()
  {
    if(!op1()) {
      … // log? clean up?
      return FAILED;
    }
    if(!op2()) {
      … // log? clean up?
      return FAILED;
    }
    …
    return SUCCEEDED;
  }

好一點的是這樣:

  int foo()
  {
    if(!op1()) goto FAILED;
    if(!op2()) goto FAILED;
    …
    if(!opN()) goto FAILED;
    return SUCCEEDED;
  FAILED:
    … // log, clean up
    return FAILED;
  }

就算是最后一種做法(所謂的“On Error Goto”),One True Path中仍然夾雜著大量的噪音(如果返回的錯誤值不只是FAILED/SUCCEEDED兩種的話噪音會更大)。此外手動檢查返回值的成功失敗畢竟是很error-prone的做法。

值得注意的是,這里我并沒有用一個常被引用的例子,即:如果你是用C寫代碼(C不支持局部變量自動析構(gòu)(RAII)),那么程序往往會被寫成這樣:

  int f()
  {
    int returnCode = FAILED;
    acquire resource1;
    if(resource1 is acquired) {
      acquire resource2;
      if(resource2 is acquired) {
        acquire resource3;
        if(resource3 is acquired) {
          if(doSomething1()) {
            if(doSomething2()) {
              if(doSomething3()) {
                returnCode = SUCCEEDED;
              }
            }
          }
          release resource3;
        }
        release resource2;
      }
      release resource1;
    }
    return returnCode;
  }

或者像這樣:

  int f()
  {
    int returnCode = FAILED;
    acquire resource1;
    if(resources1 is not acquired)
      return FAILED;
    acquire resource2;
    if(resource2 is not acquired) {
      release resource1;
      return FAILED;
    }
    acquire resource3;
    if(resource3 is not acquired) {
      release resource2;
      release resource1;
      return FAILED;
    }
    ... // do something
    release resource3;
    release resource2;
    release resource1;
    return SUCCEEDED;
  }

(一個更復(fù)雜的具體例子可以參考[16])

以上兩種方案在可伸縮性方面的問題是顯而易見的:一旦需要獲取的資源多了以后代碼也會隨著越來越難以卒讀,要么是if嵌套層次隨之線性增多,要么是重復(fù)代碼增多。所以即便項目中因為某些現(xiàn)實原因只能使用error-code,也最好采用前面提到的“On Error Goto”方案。

另一方面,當整個函數(shù)需要保持異常中立的時候,異常的優(yōu)勢就更顯現(xiàn)出來了:使用error-code,你還是需要一次次的小心check每個返回的錯誤值,從而阻止執(zhí)行流帶著錯誤往下繼續(xù)執(zhí)行。用異常的話,可以直接書寫One True Path,連try-catch都不要。

當然,即便是使用異常作為錯誤匯報機制,錯誤安全(error-safety)還是需要保證的。值得注意的是,錯誤安全性屬于錯誤處理的本質(zhì)困難,跟使用異常還是error-code來匯報錯誤沒有關(guān)系,一個常見的謬誤就是許多人把在異常使用過程中遇到的錯誤安全性方面的困難歸咎到異常身上。

3.      脆弱(易錯)。只要忘了檢查任意一個錯誤代碼,執(zhí)行流就必然會帶著錯誤狀態(tài)往下繼續(xù)執(zhí)行,后者幾乎肯定不是你想要的。帶著錯誤狀態(tài)往下執(zhí)行好一點的會立即崩潰,差一點的則在相差十萬八千里的地方引發(fā)一個莫名其妙的錯誤。

4.      難以(編寫時)確保和(review時)檢查代碼的正確性。需要檢查所有可能的錯誤代碼有沒有都被妥善check了,其中也許大部分都是不能直接對付而需要傳播給上級的錯誤。

5.      耦合。即便你的函數(shù)是一個異常中立的函數(shù),不管底層傳上來哪些錯誤一律拋給上層,你仍然需要在每個調(diào)用的邊界檢查,并妥善往上手動傳播每一個錯誤代碼。而一旦底層接口增加、減少或改動錯誤代碼的話,你的函數(shù)就需要立即作出相應(yīng)改動,檢查并傳播底層接口改動后相應(yīng)的錯誤代碼——這是很不幸的,因為你的函數(shù)只是想保持異常中立,不管底層出什么錯一律拋給上層調(diào)用方,這種情況下理想情況應(yīng)該是不管底層的錯誤語意如何修改,當前層都應(yīng)該不需要改動才對。

6.      沒有異常,根本無法編寫泛型組件。泛型組件根本不知道底層會出哪些錯,泛型組件的特點之一便是錯誤中立。但用error-code的話,怎么做到錯誤中立?泛型組件該如何檢查,檢查哪些底層錯誤?唯一的辦法就是讓所有的底層錯誤都用統(tǒng)一的SUCCEEDED和FAILED代碼來表達,并且將其它錯誤信息用GetLastError來獲取。姑且不說這個方案的丑陋,如何、由誰來統(tǒng)一制定SUCCEEDED和FAILED、GetLastError的標準?就算有這個統(tǒng)一標準,你也可以設(shè)想一下某個標準庫泛型算法(如for_each)編寫起來該是如何丑陋。

7.      錯誤代碼不可以被忘掉(忽視)。忘掉的后果見第3條。此外,有時候我們可能會故意不管某些錯誤,并用一個萬能catch來捕獲所有未被捕獲的錯誤,log,向支持網(wǎng)站發(fā)送錯誤報告,并重啟程序。用異常這就很容易做到——只要寫一個unhandled exception handler(不同語言對此的支持機制不一樣)即可。

異常與錯誤代碼的本質(zhì)區(qū)別之二——異常的傳播使用的是一個單獨的信道,而錯誤代碼則占用了函數(shù)的返回值;函數(shù)的返回值本來的語意是用來返回“有用的”結(jié)果的,這個結(jié)果是屬于程序的One True Path的,而不是用來返回錯誤的。

利用返回值來傳播錯誤導(dǎo)致的問題如下:

8.      所有函數(shù)都必須將返回值預(yù)留給錯誤。如果你的函數(shù)最自然的語意是返回一個double,而每個double都是有效的。不行,你得把這個返回值通道預(yù)留著給錯誤處理用。你可能會說,我的函數(shù)很簡單,不會出錯。但如果以后你修改了之后,函數(shù)復(fù)雜了呢?到那個時候再把返回的double改為int并加上一個double&作為out參數(shù)的話,改動可就大了。

9.      返回值所能承載的錯誤信息是有限的。NULL?-1?什么意思?具體的錯誤信息只能用GetLastError來提供… 哦,對了,你看見有多少人用過GetLastError的?

10.  不優(yōu)雅的代碼。呃…這個問題前面不是說過了么?不,這里說的是另一個不優(yōu)雅之處——占用了用來返回結(jié)果的返回值通道。本來很自然的“計算——返回結(jié)果”,變成了“計算——修改out參數(shù)——返回錯誤”。當然,你可以說這個問題不是很嚴重。的確,將double res = readInput();改為double res; readInput(&res);也沒什么大不了的。如果是連調(diào)用呢?比如,process(readInput());呃… 或者readInput() + …?或者一般地,op1(op2(), op3(), …);?

11.  錯誤匯報方案不一致性。看看Win32下面的錯誤匯報機制吧:HRESULT、BOOL、GetLastError…本質(zhì)上就是因為利用返回值通道是一個補丁方案,錯誤處理是程序的一個方面(aspect),理應(yīng)有其單獨的匯報通道。利用異常的話,錯誤匯報方案就立即統(tǒng)一了,因為這是一個first-class的語言級支持機制。

12.  有些函數(shù)根本無法返回值,如構(gòu)造函數(shù)。有些函數(shù)返回值是語言限制好了的,如重載的操作符和隱式轉(zhuǎn)換函數(shù)。

異常與錯誤代碼的本質(zhì)區(qū)別之三——異常本身能夠攜帶任意豐富的信息

13.  有什么錯誤報告機制能比錯誤報告本身就包含盡量豐富的信息更好的呢?使用異常的話,你可以往異常類里面添加數(shù)據(jù)成員,添加成員函數(shù),攜帶任意的信息(比如Java的異常類就缺省攜帶了非常有用的調(diào)用棧信息)。而錯誤代碼就只有一個單薄的數(shù)字或字符串,要攜帶其它信息只能另外存在其它地方,并期望你能通過GetLastError去查看。

異常與錯誤代碼的本質(zhì)區(qū)別之四——異常是OO的

14.  你可以設(shè)計自己的異常繼承體系。呃…那這又有什么用呢?當然有用,一個最大的好處就是你可以在任意抽象層次上catch一組異常(exception grouping),比如你可以用catch(IOException)來捕獲所有的IO異常,用catch(SQLException)來捕獲所有的SQL異常。用catch(FileException)來catch所有的文件異常。你也可以catch更明確一點的異常,如StreamEndException。總之,catch的粒度是粗是細,根據(jù)需要,隨你調(diào)節(jié)。當然了,你可以設(shè)計自己的新異常。能夠catch一組相關(guān)異常的好處就是你可以很方便的對他們做統(tǒng)一的處理。

異常與錯誤代碼的本質(zhì)區(qū)別之五——異常是強類型的

15.  異常是強類型的。在catch異常的時候,一個特定類型的catch只能catch類型匹配的異常。而用error-code的話,就跟enum一樣,類型不安全。-1 == foo()?FAILED == foo()?MB_OK == foo()?大家反正都是整數(shù)。

異常與錯誤代碼的本質(zhì)區(qū)別之六——異常是first-class的語言機制

16.  代碼分析工具可以識別出異常并進行各種監(jiān)測或分析。比如PerfMon就會對程序中的異常做統(tǒng)計。這個好處放在未來時態(tài)下或許怎么都不應(yīng)該小覷。

選擇什么錯誤處理機制?

看完上面的比較,答案相信應(yīng)該已經(jīng)很明顯了:異常。

如果你仍然是error-code的思維習慣的話,可以假想將所有error-code的地方改為拋出exception。需要注意的是,error-code不是status-code。并非所有返回值都是用來報告真正的錯誤的,有些只不過是控制程序流的。就算返回的是bool值(比如查找子串,返回是否查找到),也并不代表false的情況就是一個錯誤。具體參加上一節(jié):“哪些情況不屬于錯誤”。

一個最為廣泛的誤解就是:異常引入了不菲的開銷,而error-code沒有開銷,所以應(yīng)該使用error-code。這個論點的漏洞在于,它認為只要是開銷就是有問題的,而不關(guān)心到底是在什么情況下的開銷。實際上,現(xiàn)代的編譯器早已能夠做到異常在happy path上的零開銷。當然,空間開銷還是有的,因為零開銷方案用的是地址表方案;但相較于時間開銷,這里的空間開銷幾乎從來都不是個問題。另一方面,一旦發(fā)生了異常,程序肯定就出了問題,這個時候的時間開銷往往就不那么重要了。此外有人會說,那如果頻繁拋出異常呢?如果頻繁拋出異常,往往就意味著那個異常對應(yīng)的并非一個錯誤情況。

《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》里面一再強調(diào):不要在項目里面關(guān)閉異常支持。因為就算你的項目里面不拋出異常,標準庫也依賴于異常。一旦關(guān)閉異常,不僅你的項目代碼都要依賴于error-code(error-code的缺點見下一節(jié)),整個標準庫便也都要依賴于非標準的途徑來匯報錯誤,或者干脆就不匯報錯誤。如果你的項目是如此的硬實時,乃至于你在非常小心且深入的分析之后發(fā)覺某些操作真的負擔不起異常些微的空間開銷和unhappy path上的時間開銷的話,也要盡量別在全局關(guān)閉異常支持,而是盡量將這些敏感的操作集中到一個模塊中,按模塊關(guān)閉異常。

插曲:異常的例外情況

凡事都有例外。《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》上面陳述了兩個例外情況:一,用異常沒有帶來明顯的好處的時候:比如所有的錯誤都會在立即調(diào)用端解決掉或者在非常接近立即調(diào)用端的地方解決掉。二,在實際作了測定之后發(fā)現(xiàn)異常的拋出和捕獲導(dǎo)致了顯著的時間開銷:這通常只有兩種情況,要么是在內(nèi)層循環(huán)里面,要么是因為被拋出的異常根本不對應(yīng)于一個錯誤。

如何進行錯誤處理?

這個問題同樣極其重要。它分為三個子問題:

1.      何時拋出異常。

2.      何時捕獲異常。

3.      如何避開異常,保持異常中立(或“異常透明”)。

其中最后一個問題最為重要,屬于錯誤處理的本質(zhì)性困難之一。

先說前兩個問題。

從本質(zhì)上,錯誤分為兩種,一種是可恢復(fù)的,另一種是不可恢復(fù)的。

對于可恢復(fù)的錯誤。有兩種方案:

1.      在錯誤的發(fā)生點上立即就予以恢復(fù)。比如配置文件不存在便創(chuàng)建一個缺省的,某個配置項缺失就使用缺省值等等。這一方案的好處是當前函數(shù)不返回任何錯誤,因為錯誤被當即搞定了,就像沒發(fā)生一樣。這種方案的前提是當前函數(shù)必須要有對付該錯誤的足夠上下文,如果一個底層的函數(shù)對全局語意沒有足夠的視圖,這時就可以拋出異常,由上層函數(shù)負責恢復(fù)。

2.      在某個上層棧上恢復(fù)。這種情況下,在負責恢復(fù)的那層棧以下的調(diào)用一般被看成一個整體事務(wù),其中發(fā)生的任何錯誤都導(dǎo)致整個事務(wù)回滾,回滾到錯誤恢復(fù)棧層面時,由相應(yīng)的catch子句進行恢復(fù),并重新執(zhí)行整個事務(wù),或者將程序引向另一條備選路徑(alternative)。

對于不可恢復(fù)的錯誤。也有兩種方案:

1.      Sudden Death。在錯誤的發(fā)生點上退出模塊(可能伴隨著重啟模塊)。退出模塊前往往需要先釋放資源、保存關(guān)鍵數(shù)據(jù)、記錄日志,等等。該方案的前提是在錯誤的發(fā)生點的上下文中必須要能夠釋放所有資源,要能夠保存關(guān)鍵數(shù)據(jù)。要滿足這個前提,可以用一個全局的沙盒來保存整個模塊到當前為止申請的所有資源,從而在任何出錯點上都可以將這個沙盒整個釋放掉。也可以用智能垃圾收集,這樣在出錯點上只要記錄日志和保存數(shù)據(jù),把掃尾工作留給智能垃圾收集器完成。這個方案的弱點是如果釋放資源是要按某種次序的就比較麻煩。

2.      回滾。如果你并沒有用智能垃圾收集(要智能到能夠回收文件句柄,網(wǎng)絡(luò)端口等,不光是內(nèi)存),或者你并沒有在某個全局可訪問的位置保存到當前為止模塊申請的所有資源,或者你的資源互相之間有依賴關(guān)系,必須按照分配的逆序釋放,等等,那么就必須按照調(diào)用棧的反方向回滾事務(wù)。回滾到一個所謂的Fault Barrier,用一個catch-all在那里等著,所謂Fault-Barrier的作用就是為了抓這些沒法妥善恢復(fù)的錯誤的,它做的事情通常就是logging、發(fā)送錯誤報告、可能也會重啟模塊。Fault Barrier一般在一個內(nèi)聚的單一職責的功能模塊的邊界出現(xiàn)。

嚴格來說,其實還有第三種情況,即編寫當前代碼的時候并不確定某個錯誤是否能被恢復(fù)。這種時候拋出一個異常往往是最靈活的選擇,因為在沒有想好恢復(fù)方案的時候,上層調(diào)用對該異常都是中立的。一旦后面想好恢復(fù)方案了,不管是在某個上層調(diào)用內(nèi)捕獲該異常,還是在最底層錯誤發(fā)生點上就立即解決錯誤從而根本取消掉該異常,都沒有問題。

異常轉(zhuǎn)換

在如何拋出和捕獲異常的問題上,還有一個子問題就是異常的轉(zhuǎn)換(translation)。以下情況下應(yīng)該轉(zhuǎn)換一個由底層傳上來的異常:

1.      拋出一個對應(yīng)于當前抽象層的異常。比如Document::open當接收到底層的文件異常(或數(shù)據(jù)庫異常,網(wǎng)絡(luò)異常,取決于這個Document來自何方)時,將其轉(zhuǎn)換為“文檔無效或被破壞”異常,增加高層語意,并避免暴露底層實現(xiàn)細節(jié)(對異常的一個批評就是會暴露內(nèi)部實現(xiàn),而實際上,通過適當轉(zhuǎn)換異常,可以使得異常總位于當前的抽象層次上,成為接口的一部分)。

2.      在模塊邊界上。如果一個模塊,在內(nèi)部使用異常,但在邊界上必須提供C API的話,就必須在邊界上捕獲異常并將其轉(zhuǎn)換為error-code。

沒有時間機器——錯誤處理的本質(zhì)困難

剛才提到“回滾”。那么,在異常發(fā)生的時候,如何回滾既然已經(jīng)發(fā)生的操作?這就是要說的第三個問題:如何在異常從發(fā)生點一路傳播到捕獲點的路徑上保持異常中立,即回滾操作,釋放資源。簡而言之就是要做到錯誤安全(error-safe)。錯誤安全攸關(guān)強異常安全保證和事務(wù)語意。在異常編程里面,錯誤安全是最重要的一環(huán)。

理想情況下,我們要的是一個時間機器:打碎的杯子要能還原,釋放的內(nèi)存要能重新得到,銷毀的對象就像沒銷毀前一模一樣,發(fā)射的導(dǎo)彈就像從來也沒有離開發(fā)射筒一樣,數(shù)據(jù)庫就像從來沒被寫入一樣…

沒有時間機器。

那么,如何回滾木已成舟的操作?

目前有兩個主要方案:

1.      Discard(丟棄):一個例子就能夠說明這種做法,源自STL的“copy-swap手法”。比如一個vector,你要往里面插入一個元素,如果插入元素失敗的話你想要vector維持原狀,就好像從來沒有動過一樣。如何做到這一點呢?你可以先把這個vector拷貝一份,往拷貝里面插入元素,然后將兩個vector調(diào)換(swap)一下即可,swap是不會失敗的,因為它只是把兩個指針互換了一下。而如果往那個拷貝里面插入元素失敗的話,拷貝就會被Discard(丟棄掉),不會帶來任何實際的副作用。當然,這種做法是有代價的,誰叫你要強異常安全保證呢?再比如一個拷貝賦值操作符可以這樣寫:MyClass(other).swap(*this); 當然,前提還是swap()必須具有標準的no-throw語意。

這種做法一般化的描述就是:“在一個‘副本’里把所有的事情都做好了,然后用一個不會出錯的函數(shù)提交(commit)”。這樣一來,中途出了任何錯誤只要丟棄那個副本即可(往往只要任其析構(gòu))。要做到這一點,一個原則就是:“在破壞一份信息之前要確保其新的版本一定能夠無錯的替換掉原信息”,例如在拷貝構(gòu)造函數(shù)中,不能先delete再new,因為new可能失敗,一旦new失敗了,delete掉的信息可就找不回來了。

2.      Undo(撤銷):有時候,你一方面不想付出Discard方案的(通常不菲的)空間開銷,另一方面你又想擁有強異常安全保證。這也是有辦法的,比如:

  void World::addPerson(Person const& person)
  {
    m_persons.push_back(person);
    scope(failure) { m_persons.pop_back(); }
    … // other operations
  }

scope(failure)是D語言的特性,其語意顯而易見:如果當前的scope以失敗(異常)退出的話,{}內(nèi)的語句就被執(zhí)行。這么一來,在上面的例子中,如果后續(xù)的操作失敗,那么這個person就會被從m_persons中pop_back出來,一次事務(wù)撤銷,使得這個函數(shù)對m_persons的影響歸零。

該方案的前提有兩個:一,回滾操作(比如這里的m_persons.pop_back())必須存在且不會失敗(no-throw)。比如missile.lunch()就不能回滾,操作系統(tǒng)API一般也無法回滾;I/O操作也無法回滾;另一方面,內(nèi)存操作一般而言都是可以回滾的,只要回復(fù)原來內(nèi)存的值即可。二,被回滾的操作(比如這里的m_persons.push_back(person))也一定要是強異常保證的。比如在中間而不是尾部插入一個person(m_persons.insert(iter, person))就不是強保證的,這種時候就要訴諸前一個方案(Discard)。

D的scope(failure)(還有scope(exit)、scope(success))是非常強大的設(shè)施。利用它,一個具有事務(wù)語意的函數(shù)的一般模式如下:

  void Transaction()
  {
    op1; // strong operation
    scope(failure) { undo op1; }
    op2; // strong operation
    scope(failure) { undo op2; }
    …
    opN; // strong operation
    scope(failure) { undo opN; }
  }

在C++里面也可以模擬D的scope(failure),Andrei Alexandrescu曾實現(xiàn)了一個ScopeGuard類[11],而旨在完全模擬D的scope特性的boost.scope-exit也在review中。只不過C++03里面的模擬方案有一些學習曲線和使用注意點,C++09之后會有更方便的方案。在其它不支持scope(failure)的語言中,也可以模擬這種做法,不過做法很笨拙。

3.      呃… 哪來的第三個方案?前面不是說了只有兩個方案嗎?是的。因為這第三個方案是“理想”方案,目前還沒有進入主流語言,不過在haskell里面已經(jīng)初見端倪了。強異常安全保證的核心思想其實就是事務(wù)語意,而事務(wù)語意的核心思想就是“不成功便成仁”(這個思想有許多有趣的說法,比如:“武士道原則”、“要么直著回來要么橫著回來”、“干不了就去死”),根據(jù)這個想法,其實最簡單的方案是把一組屬于同一事務(wù)的操作簡單地圈起來(標記出來),把回滾操作留給語言實現(xiàn)去完成:

  stm {
    op1;
    op2;
    …
    opN;
  }

只要op1至opN中任意一個失敗,整個stm塊對內(nèi)存的寫操作就全被自動拋棄(這要求編譯器和運行時的支持,一般是用一個緩沖區(qū)或?qū)懭罩緛韺崿F(xiàn)),然后異常被自動拋出這個stm塊之外。這個方案的優(yōu)點是它太優(yōu)美了,我們幾乎只要關(guān)注One True Path即可,唯一要做的事情就是用stm{…}圈出代碼中需要強保證的代碼段。這個方案的缺點有兩個:一,它跟第一個方案一樣,有空間開銷,不過空間開銷通常要小一點,因為只要緩存特定的寫操作。二,當涉及到操作系統(tǒng)API,I/O等“外部”操作的時候,底層實現(xiàn)就未必能夠回滾這些操作了。另一個理論上的可能性是,當回滾操作和被回滾操作并非嚴格物理對應(yīng)(所謂物理對應(yīng)就是說,回滾操作將內(nèi)存回滾到目標操作發(fā)生之前的狀態(tài))的時候,底層實現(xiàn)也不知道如何回滾。

(STM(軟件事務(wù)內(nèi)存)目前在haskell里面實現(xiàn)了,Intel也釋出了C/C++的STM預(yù)覽版編譯器。只不過STM原本的意圖是實現(xiàn)鎖無關(guān)算法的。后者就是另一個話題了。)

RAII

實際上剛才還有一個問題沒有說,那就是如何確保資源一定會被釋放(即便發(fā)生異常),這在D里面對應(yīng)的是scope(exit),在Java里面對應(yīng)的是finally,在C#   里面對應(yīng)的是scoped using。

簡而言之就是,不管當前作用域以何種方式退出,某某操作(通常是資源釋放)都一定要被執(zhí)行。

這個問題的答案其實C++程序員們應(yīng)該耳熟能詳了:RAII。RAII是C++最為強大的特性之一。在C++里面,局部變量的析構(gòu)函數(shù)剛好滿足這個語意:無論當前作用域以何種方式退出,所有局部變量的析構(gòu)函數(shù)都必然會被倒著調(diào)用一遍。所以只要將有待釋放的資源包裝在析構(gòu)函數(shù)里面,就能夠保證它們即便在異常發(fā)生的情況下也會被釋放掉了。為此C++提供了一系列的智能指針:auto_ptr、scoped_ptr、scoped_array… 此外所有的STL容器也都是RAII的。在C++里面模擬D的scope(exit)也是利用的RAII。

RAII相較于java的finally的好處和C#的scoped using的好處是非常明顯的。只要一段代碼就高下立判:

  // in Java
  String ReadFirstLineFromFile( String path )
  {
    StreamReader r = null;
    String s = null;
    try {
      r = new StreamReader(path);
      s = r.ReadLine();
    } finally {
      if ( r != null ) r.Dispose();
    }
    return s;
  }

  // in C#
  String ReadFirstLineFromFile( String path )
  {
    using ( StreamReader r = new StreamReader(path) ) {
      return r.ReadLine();
    }
  }

顯然,Java版本的(try-finally)最臃腫。C#版本(scoped using)稍微好一些,但using畢竟也不屬于程序員關(guān)心的代碼邏輯,仍然屬于代碼噪音;況且如果不連續(xù)地申請N個資源的話,使用using就會造成層層嵌套結(jié)構(gòu)。

如果使用RAII手法來封裝StreamReader類的話(std::fstream就是RAII類的一個范例),代碼就簡化為:

  // in C++, using RAII
  String ReadFirstLineFromFile(String path)
  {
    StreamReader r(path);
    return r.ReadLine();
  }

好處是顯而易見的。完全不用擔心資源的釋放問題,代碼也變得“as simple as possible”。此外,值得注意的是,以上代碼只是演示了最簡單的情況,其中需要釋放的資源只有一個。其實這個例子并不能最明顯地展現(xiàn)出RAII強大的地方,當需要釋放的資源有多個的時候,RAII的真正強大之處才被展現(xiàn)出來,一般地說,如果一個函數(shù)依次申請N個資源:

  void f()
  {
    acquire resource1;
    …
    acquire resource2;
    …
    acquire resourceN;
    …
  }

那么,使用RAII的話,代碼就像上面這樣簡單。無論何時退出當前作用域,所有已經(jīng)構(gòu)造初始化了的資源都會被析構(gòu)函數(shù)自動釋放掉。然而如果使用try-finally的話,f()就變成了:

  void f()
  {
    try {
      acquire resource1;
      … // #1
      acquire resource2;
      … // #2
      acquire resourceN;
      … // #N
    } finally {
      if(resource1 is acquired) release resource1;
      if(resource2 is acquired) release resource2;
      …
      if(resourceN is acquired) release resourceN;
    }
  }

為什么會這么麻煩呢,本質(zhì)上就是因為當執(zhí)行流因異常跳到finally塊中時,你并不知道執(zhí)行流是從#1處、#2處…還是#N處跳過來的,所以你不知道應(yīng)該釋放哪些資源,只能挨個檢查各個資源是否已經(jīng)被申請了,如果已申請了便將其釋放;要能夠檢查每個資源是否已經(jīng)被申請了,往往就要求你要在函數(shù)一開始將各個資源的句柄全都初始化為null,這樣才可以通過if(hResN==null)來檢查第N個資源是否已經(jīng)申請。

最后,RAII其實是scope(exit)的特殊形式。但在資源釋放方面,RAII有其特有的優(yōu)勢:如果使用scope(exit)的話,每個資源分配之后都需要用一個scope(exit)跟在后面保護起來;而如果用RAII的話,一個資源申請就對應(yīng)于一個RAII對象的構(gòu)造,釋放工作則被隱藏在對象的析構(gòu)函數(shù)中,從而使代碼主干保持了清爽。

總結(jié)

本文討論了錯誤處理的原因、時機和方法。錯誤處理是編程中極其重要的一環(huán);也是最被忽視的一環(huán),Gosling把這個歸因于大多數(shù)程序員在學校的時候都是做著大作業(yè)當編程練習的,而大作業(yè)鮮有老師要求要妥善對待錯誤的,大家都只要“work on the one true path”即可;然而現(xiàn)實世界的軟件可必須面對各種各樣的意外情況,往往一個程序的錯誤處理機制在第一次面對錯誤的時候便崩潰了… 另一方面,錯誤處理又是一個極其困難的問題,其本質(zhì)困難來源于兩個方面:一,哪些情況算是錯誤。二,如何做到錯誤安全(error-safe)。相較之下在什么地點解決錯誤倒是容易一些了。本文對錯誤處理的問題作了詳細的分析,總結(jié)了多年來這個領(lǐng)域爭論的結(jié)果,提供了一個實踐導(dǎo)引。

下期預(yù)告

關(guān)于在C++里面為什么不應(yīng)該使用異常規(guī)格聲明(exception specification),參考文章后面的延伸閱讀[5]。

關(guān)于如何設(shè)計異常繼承體系,checked exception/unchecked exception之爭,關(guān)于異常處理的迷思、謬論、誤解,等等同樣有意思的問題,請聽下回分解(如果有下回的話:-))。

延伸閱讀

[1] 《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》,“Error Handling and Exceptions”部分,第68至75章。

[2] 《The C++ Programming Language, 3rd special edition》,第14章,尤其是14.4.7、14.5、14.8。第21章的21.3.6。以及附錄E。

[3] 《The Design and Evolution of C++》,整個16章。

[4] 《Exceptional C++》,整本書。

[5] 《Exceptional C++ Style》,第11、12、13章。

[6] 《The Java Programming Language, 4th edition》,第12章。

[7] 《Effective Java》,Item39至Item47。

[8] 《Object Oriented Software Construction, 2nd edition》,第11、12章。

文章

[1] B. Stroustrup: Programming with Exceptions(pdf)

[2] B. Stroustrup: Exception Safety: Concepts and Techniques

[3] D. Abrahams: Error and Exception Handling

[4] D. Abrahams: Exception-Safety in Generic Components

[5] Tom Cargill: Exception Handling, A False Sense of Security

[6] Damien Katz: Error codes or Exceptions? Why is Reliable Software so Hard?

[7] Alan Griffiths: Here be Dragons

[8] Ned Batchelder: Exceptions vs. status returns

[9] Ned Batchelder: Exceptions in the rainforest

[10] Ned Batchelder: Fix error handling first

[11] A. Alexandrescu, P. Marginean: Change the Way You Write Exception-Safe Code — Forever

[12] Brad Abrams: Exceptions and Error Codes

[13] A. Alexandrescu: Exception Safety Analysis(pdf)

[14] Brian Goetz: Exceptional practices

[15] James Gosling: Failure and Exceptions

[16] Sun: Advantages of Exceptions

[17] Vishal Kochhar: How a C++ compiler implements exception handling

(注:以上皆是與本文相關(guān)的,與下一個議題相關(guān)的文章就暫不列出了)

本文來自CSDN博客,轉(zhuǎn)載請標明出處:http://blog.csdn.net/pongba/archive/2007/10/08/1815742.aspx

posted on 2010-06-12 11:01 溪流 閱讀(627) 評論(0)  編輯 收藏 引用 所屬分類: C++
青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            久久精品久久综合| 亚洲免费观看高清在线观看| 欧美一级大片在线观看| 欧美在线观看网站| 91久久线看在观草草青青| 中文一区字幕| 激情综合亚洲| 亚洲精品五月天| 国产一区av在线| 夜夜嗨av一区二区三区网页| 国产午夜精品美女视频明星a级| 亚洲国产成人精品女人久久久 | 日韩视频在线观看| 亚洲天堂av在线免费| 日韩一二在线观看| 久久精品99国产精品日本| 西西裸体人体做爰大胆久久久| 免费在线成人av| 久久福利毛片| 国产精品视频最多的网站| 黄色成人精品网站| 一区二区三区产品免费精品久久75 | 亚洲日本欧美| 香港久久久电影| 久久av一区二区三区| 国产一区二区三区免费不卡| 日韩视频在线观看| 一二三区精品福利视频| 欧美成人精品一区二区三区| 久久免费精品视频| 在线观看视频亚洲| 老司机亚洲精品| 91久久夜色精品国产九色| 狠狠狠色丁香婷婷综合激情| 欧美专区18| 欧美成人福利视频| 日韩视频国产视频| 国产精品日韩二区| 久久久999成人| 一区二区三区.www| 久久婷婷人人澡人人喊人人爽| 狠狠v欧美v日韩v亚洲ⅴ| 欧美成人午夜激情在线| 亚洲天堂av电影| 久久一区亚洲| 亚洲一区二区日本| 狠狠色综合网| 国产精品久久久久久五月尺| 先锋a资源在线看亚洲| 亚洲精品视频免费在线观看| 午夜视频一区在线观看| 亚洲国产日本| 伊人男人综合视频网| 欧美va天堂va视频va在线| 亚洲一二三区在线观看| 国产精品永久入口久久久| 欧美片网站免费| 久久精品色图| 久久激情一区| 免费在线观看成人av| 久久久久一本一区二区青青蜜月| 中文久久乱码一区二区| 99精品国产一区二区青青牛奶| 亚洲成人资源| 日韩一二三在线视频播| 9色国产精品| 午夜精品999| 久久精品五月| 欧美日韩国产美| 国产欧美日韩专区发布| 国产亚洲毛片| 在线日韩一区二区| 一区二区三区四区国产| 亚洲欧美成aⅴ人在线观看| 午夜精品理论片| 欧美成人午夜77777| 日韩一级成人av| 欧美一区亚洲二区| 欧美www在线| 国产一区二区三区最好精华液| 国产一区二区三区自拍| 一区二区免费看| 欧美77777| 亚洲欧美日韩人成在线播放| 久久综合色8888| 国产日韩欧美一区| 亚洲色图在线视频| 欧美激情第五页| 午夜精品www| 国产精品青草久久| 日韩小视频在线观看专区| 久久久久.com| 亚洲欧美春色| 欧美午夜精品久久久久久浪潮| 亚洲国产高清高潮精品美女| 欧美日本在线| 在线欧美日韩国产| 久久精品亚洲乱码伦伦中文| 一区二区三区你懂的| 欧美精品手机在线| 亚洲最新视频在线| 亚洲国产毛片完整版| 欧美大片第1页| 亚洲国产精品久久久久婷婷老年 | 国产精品亚洲综合| 亚洲午夜在线| 欧美大片91| 欧美gay视频激情| 国产午夜精品一区二区三区欧美 | 在线成人av网站| 亚洲午夜激情| 亚洲女优在线| 欧美少妇一区| 亚洲毛片一区| 亚洲影音先锋| 国产精品久久久久久久久久免费看| 亚洲国产专区| 日韩视频欧美视频| 麻豆91精品| 亚洲精品影院在线观看| 亚洲美女一区| 久久婷婷亚洲| 久久aⅴ国产紧身牛仔裤| 玖玖玖国产精品| 午夜久久美女| 国产精品成人v| 亚洲另类一区二区| 一区二区自拍| 欧美在线观看一区二区| 一区二区黄色| 欧美激情一区二区三区蜜桃视频 | 欧美有码视频| 亚洲欧洲日产国产综合网| 欧美亚洲视频一区二区| 久久精品99国产精品| 国产精品高清在线| 久久狠狠久久综合桃花| 久久精品国产99| 国产亚洲一区二区三区在线播放 | 国产一区二区三区四区五区美女| 亚洲精品社区| 亚洲主播在线观看| 国产亚洲精品bv在线观看| 欧美亚洲视频一区二区| 欧美xart系列高清| 日韩午夜视频在线观看| 欧美三级视频在线播放| 亚洲一区二区三区免费观看 | 国产欧美精品在线播放| 亚洲午夜精品在线| 欧美成va人片在线观看| 亚洲少妇最新在线视频| 国产欧美一区二区精品婷婷| 久久免费视频在线观看| 亚洲国产精品黑人久久久| 香蕉成人啪国产精品视频综合网| 亚洲成人影音| 国产伦精品一区二区三区四区免费| 久久久久久网址| 亚洲麻豆一区| 美女被久久久| 久久一二三区| 午夜在线电影亚洲一区| 亚洲电影av| 欧美精品乱码久久久久久按摩| 这里只有精品丝袜| 欧美成人免费在线视频| 欧美在线亚洲在线| 韩国av一区二区| 国产精品久久7| 国产日本欧美一区二区三区| 国产日韩欧美一区二区| 久久九九久精品国产免费直播| 久久天堂国产精品| 欧美日本国产在线| 国产婷婷色综合av蜜臀av| 亚洲电影天堂av| 午夜在线视频观看日韩17c| 欧美日韩一区二区在线观看视频| 欧美人与禽猛交乱配视频| 国产精品高精视频免费| 好看的日韩视频| 中文日韩在线| 麻豆91精品| 韩日欧美一区二区三区| 性欧美在线看片a免费观看| 亚洲一区精品电影| 久久久综合激的五月天| 亚洲精品自在久久| 久久久久天天天天| 国产精品国产三级国产| 精品va天堂亚洲国产| 亚洲免费视频成人| 男人的天堂成人在线| 亚洲——在线| 欧美日韩亚洲一区二区三区在线 | 久久精品国产精品亚洲| 亚洲国产精品久久91精品| 亚洲午夜一二三区视频| 国产精品成人国产乱一区|