??xml version="1.0" encoding="utf-8" standalone="yes"?>国产成人久久精品一区二区三区 ,亚洲综合久久综合激情久久 ,国产激情久久久久影院老熟女http://www.shnenglu.com/alexhappy/category/11525.htmlFaith Firstzh-cnMon, 24 Aug 2009 10:19:24 GMTMon, 24 Aug 2009 10:19:24 GMT60C++错误处理http://www.shnenglu.com/alexhappy/articles/93906.htmlalexhappyalexhappyThu, 20 Aug 2009 05:35:00 GMThttp://www.shnenglu.com/alexhappy/articles/93906.htmlhttp://www.shnenglu.com/alexhappy/comments/93906.htmlhttp://www.shnenglu.com/alexhappy/articles/93906.html#Feedback0http://www.shnenglu.com/alexhappy/comments/commentRss/93906.htmlhttp://www.shnenglu.com/alexhappy/services/trackbacks/93906.html
 

By 刘未?pongba)

C++的罗宫(http://blog.csdn.net/pongba)

TopLanguage(http://groups.google.com/group/pongba)

 

引言

错误处理QError-HandlingQ这个重要议题从1997q_(d)也许更早Q到2004q左右一直是一个被q泛争论的话题,曑֜新闻l上、博客上、论坛上引发口水无数Q不亚于语言之争Q,Bjarne Stroustrup、James Gosling、Anders Hejlsberg、Bruce Eckel、Joel Spolsky、Herb Sutter、Andrei Alexandrescu、Brad Abrams、Raymond Chen、David Abrahams…Q各路神仙纷U出动,好不热闹:-)

 

如今争论虽然已经基本l束q结果;只不q结论散落在大量文献当中Q且新旧文献陈杂Q如果随便翻看其中的几篇乃至几十的话都隑օ中Hv。就qGosling本h写的《The Java Programming Language》中也语焉不详。所以,写这文章的目的便是要对q个问题提供一个整体视图,怿我,q是个有的话题:-)

 

Z么要错误处理Q?br>
q是关于错误处理的问题里面最单的一个。答案也很简单:(x)现实世界是不完美的,意料之外的事情时有发生?br>
 

一个现实项目不像在学校里面完成大作业,只要考虑该完成的功能Q走happy pathQ也叫One True PathQ即可,忽略M可能出错的因素(?. 你会(x)_(d)怎么?x)出错呢Q配|文件肯定在那,矩阵文g里面肯定含的是有效数?. 因ؓ(f)所有的环境因素都在你的控制之下。就出C么不,比如q行C半被人拔?jin)网U,那就让程序崩溃好?jin),再双M下不p?jin)嘛Q?br>
 

然而现实世界的软g必考虑错误处理?jin)。如果一个错误是能够恢复的,要尽量恢复。如果是不能恢复的,要妥善的退出模块,保护用户数据Q清理资源。如果有必要的话应该记录日志Q或重启模块{等?br>
 

而言之,错误处理的最主要目的是ؓ(f)?jin)构造健壮系l?br>
 

什么时候做错误处理Q(或者:(x)什么是“错误”Q)(j)

错误Q很单,是不正的事情。也是不该发生的事情。有一个很好的办法可以扑և哪些情况是错误。首先就当自己是在一个完环境下~程的,一切precondition都满I(x)文g存在那里Q文件没被破坏,|络永远完好Q数据包永远完整Q程序员永远不会(x)拿脑袋去打苍蝇,{等… q种情况下编写的E序也被UCؓ(f)happy pathQ或One True PathQ?br>
 

剩下的所有情况都可以看作是错误。即“不应?#8221;发生的情况,不在计之内的情况,或者预料之外的情况Qwhatever?br>
 

而言之,什么错误呢Q调用方q反被调用函数的precondition、或一个函数无法维持其理应l持的invariants、或一个函数无法满_所调用的其它函数的precondition、或一个函数无法保证其退出时的postconditionQ以上所有情况都属于错误。(详见《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》第70章,或《Object Oriented Software Construction, 2nd edition》第11?2章)(j)

 

例如文g找不刎ͼ通常意味着一个错误)(j)、配|文件语法错误、将一个Dl一个d该是正值的变量、文件存在但׃讉K限制而不能打开Q或打开不能写、网l传输错误、网l中断、数据库q接错误、参数无效等?br>
 

不过话说回来Q现实世界中Q错误与非错误之间的界定其实是很模糊的。例如文件缺失,可能大多数情况下都意味着一个错误(影响E序正常执行q得出正常结果)(j)Q然而有的情况下也可能根本就不是错误Q或者至是可恢复的错误Q,比如你的街机模拟器的配置文g~失?jin),一般程序只要创Z个缺省的卛_?br>
 

因此Q关于把哪些情况界定为错误,具体的答案几乎L视具体情况而定的。但话虽如此Q仍然还是有一些一般性的原则Q如下:(x)

 

哪些情况不属于错误?

1.      控制E序的q回g是错误:(x)如果一个情늻常发生,往往意味着它是用来控制E序程的,应该用status-codeq回Q注意,不同于error-codeQ,比如l典的while(cin >> i)。读入整型失败是很常见的情况Q而且Q这里的“d整型p|”其实真正的含义是“的下一个字D不是整?#8221;Q后者很明确C代表一个错误;再比如在一个字W串中寻找一个子Ԍ如果找不到该子串Q也不算错误。这cL制程序流的返回值都有一个共同的特点Q即我们都一定会(x)利用它们的返回值来~写if-elseQ@环等控制l构Q如Q?br>
  if(foo(…)) { … }
  else { … }

?br>
  while(foo(…)) { … }

q里再摘两个相应的具体例子,一个来自Gosling的《The Java Programming Language》,是关于stream的?br>
使用status-codeQ?br>
  while ((token = stream.next()) != Stream.END)
      process(token);
  stream.close();

使用exceptionQ?br>
  try {
    for(;;) {
      process(stream.next());
    }
  } catch (StreamEndException e) {
    stream.close();
  }

高下立判?br>
另一个例子来自TC++PLQWell, not exactlyQ:(x)

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

使用status-codeQ?br>
  int index = find(str, sub_str)
  if(index != -1) {
    … // case 1
  } else {
    … // case 2
  }

以上q类情况的特Ҏ(gu)Q返回值本w也是程序主逻辑Qhappy pathQ的一部分Q返回的两种或几U可能性,都是完全正常的、预料之中的?br>
1’. 另一斚wQ还有一U情况与此有微妙的区别,?#8220;可恢复错?#8221;。可恢复错误与上面的情况的区别在于它虽说也是预料之中的,但它一旦发生程序往往׃(x)转入一个错误恢复子q程Q后者会(x)可能恢复程序主q执行所需要的某些条gQ恢复成功程序则再次转入d执行Q而一旦恢复失败的话就真的成了(jin)一个货真h(hun)实的让h只能q瞪眼的错误?jin)。比如C++里面的operator new如果p|的话?x)尝试调用一个可自定义的错误恢复子过E,当然Q后者ƈ非总能成功程序恢复过来。除?jin){入一个错误恢复子q程之外Q另一U可能性是E序?x)degenerate入一条second-class的支,后者也许能完成某些预期的功能,但却?#8220;不完?#8221;地完成的?br>
q类错误如何处理后面?x)讨论?br>

2.      ~程bug不是错误。属于同一个hl护的代码,或者同一个小l维护的代码Q如果里面出现bugQ得一个函数的precondition得不到满I那么不应该视为错误。而应该用assert来对付。因为编Ebug发生Ӟ你不?x)希望栈回滚Q而是希望E序在assertp|点上直接中断Q调用调试程序,查看中断点的E序状态,从而解决代码中的bug?br>
关于q一点,需要尤其注意的是,它的前提是:(x)必须要是同一个h或小l维护的代码。同一个小l内可以保证查看到源代码Q进行debug。如果调用方和被调用方不属于同一负责人,则不能满precondition的话应该抛出异常。MC一个精:(x)assert是用来辅助debug的(assert的另一个作用是文档Q描q程序在特定点上的状态,即便assert被关闭,q个描述功能也依然很重要Q?br>
注意Q有时候,Z(jin)效率原因Q也?x)在W三方库里面使用assert而不用异常来报告q反precondition。比如strcpyQstd::vector的operator[]?br>

3.      频繁出现的不是错误。频J出现的情况有两U可能,一是你的程序问题大?jin)(不然怎么L出错呢?Q。二是出现的Ҏ(gu)不是错误Q而是属于E序的正常流E。后者应该改用status-code?br>
 

插曲Q异常(exceptionQvs错误代码Qerror-codeQ?br>
异常相较于错误代码的优势太多?jin),以下是一个(不完全)(j)列表?br>
 

异常与错误代码的本质区别之一——异怼(x)自动往上层栈传播:(x)一旦异常被抛出Q执行流q即中止,取而代之的是自动的stack-unwinding操作Q直到找C个适当的catch子句?br>
 

相较之下Q用error-code的话Q要下层调用发生的错误传播C层去Q就必须手动(g)查每个调用边界,M错误Q都必须手动转发Q返回)(j)l上层,E有遗漏Q就?x)带着错误的状态l往下执行,从而在下游不知道离?jin)多q的地方最l引爆程序。这来了(jin)以下几个问题Q?br>
 

1.      ȝ(ch)。每一个可能返回错误代码的调用边界都需要检查,不管你实际上对不对返回的错误作响应,因ؓ(f)即便你自׃解决q回的错误,也要把它传播C层去好让上层解决?br>

2.      不优雅且不可伸羃(scalability)的代码(错误处理代码跟One True PathQ也叫happy pathQ搅和在一P(j)。关于这一条普遍的都不够明,比如有h可能?x)反驌Q那错误反正是要(g)查的Q用异常N׃需要捕获异怺(jin)吗?当然是需要的Q但关键是,有时候我们不一定会(x)在异常发生的立即点上捕获q处理异常。这时候,异常的优势就昄出来?jin),比如Q?br>
  void foo()
  {
    try {
      op1;
      op2;
      …
      opN;
    } catch (…) {
      … // log
      … // clean up
      throw;
    }
  }

如果用error-code的话Q?br>
  int foo()
  {
    if(!op1()) {
      … // log? clean up?
      return FAILED;
    }
    if(!op2()) {
      … // log? clean up?
      return FAILED;
    }
    …
    return SUCCEEDED;
  }

好一点的是这P(x)

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

q是最后一U做法(所谓的“On Error Goto”Q,One True Path中仍然夹杂着大量的噪韻I如果q回的错误g只是FAILED/SUCCEEDED两种的话噪音?x)更大?j)。此外手动检查返回值的成功p|毕竟是很error-prone的做法?br>
值得注意的是Q这里我q没有用一个常被引用的例子Q即Q如果你是用C写代码(C不支持局部变量自动析构(RAIIQ)(j)Q那么程序往往?x)被写成q样Q?br>
  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;
  }

或者像q样Q?br>
  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;
  }

Q一个更复杂的具体例子可以参考[16]Q?br>
以上两种Ҏ(gu)在可伸羃性方面的问题是显而易见的Q一旦需要获取的资源多了(jin)以后代码也会(x)随着来难以卒读,要么是if嵌套层次随之U性增多,要么是重复代码增多。所以即侉K目中因ؓ(f)某些现实原因只能使用error-codeQ也最好采用前面提到的“On Error Goto”Ҏ(gu)?br>
另一斚wQ当整个函数需要保持异怸立的时候,异常的优势就更显现出来了(jin)Q用error-codeQ你q是需要一ơ次的小?j)check每个q回的错误|从而阻止执行流带着错误往下l执行。用异常的话Q可以直接书写One True PathQ连try-catch都不要?br>
当然Q即便是使用异常作ؓ(f)错误汇报机制Q错误安全(error-safetyQ还是需要保证的。值得注意的是Q错误安全性属于错误处理的本质困难Q跟使用异常q是error-code来汇报错误没有关p,一个常见的谬误是许多人把在异怋用过E中遇到的错误安全性方面的困难归咎到异常n上?br>

3.      脆弱Q易错)(j)。只要忘?jin)检查Q意一个错误代码,执行就必然?x)带着错误状态往下l执行,后者几乎肯定不是你惌的。带着错误状态往下执行好一点的?x)立卛_溃,差一点的则在相差十万八千里的地方引发一个莫名其妙的错误?br>

4.      难以Q编写时Q确保和QreviewӞ(j)(g)查代码的正确性。需要检查所有可能的错误代码有没有都被妥善check?jin),其中也许大部分都是不能直接对付而需要传播给上的错误?br>

5.      耦合。即便你的函数是一个异怸立的函数Q不底层传上来哪些错误一律抛l上层,你仍焉要在每个调用的边界检查,q妥善往上手动传播每一个错误代码。而一旦底层接口增加、减或改动错误代码的话Q你的函数就需要立即作出相应改动,(g)查ƈ传播底层接口改动后相应的错误代码——这是很不幸的,因ؓ(f)你的函数只是想保持异怸立,不管底层Z么错一律抛l上层调用方Q这U情况下理想情况应该是不底层的错误语意如何修改Q当前层都应该不需要改动才寏V?br>

6.      没有异常Q根本无法编写泛型组件。泛型组件根本不知道底层?x)出哪些错,泛型lg的特点之一便是错误中立。但用error-code的话Q怎么做到错误中立Q泛型组件该如何(g)查,(g)查哪些底层错误?唯一的办法就是让所有的底层错误都用l一的SUCCEEDED和FAILED代码来表达,q且其它错误信息用GetLastError来获取。姑且不说这个方案的丑陋Q如何、由谁来l一制定SUCCEEDED和FAILED、GetLastError的标准?q有这个统一标准Q你也可以设想一下某个标准库泛型法Q如for_eachQ编写v来该是如何丑陋?br>

7.      错误代码不可以被忘掉Q忽视)(j)。忘掉的后果见第3条。此外,有时候我们可能会(x)故意不管某些错误Qƈ用一个万能catch来捕h有未被捕L(fng)错误QlogQ向支持|站发送错误报告,q启程序。用异常q就很容易做到——只要写一个unhandled exception handlerQ不同语aҎ(gu)的支持机制不一P(j)卛_?br>
 

异常与错误代码的本质区别之二——异常的传播使用的是一个单独的信道Q而错误代码则占用?jin)函数的q回|函数的返回值本来的语意是用来返?#8220;有用?#8221;l果的,q个l果是属于程序的One True Path的,而不是用来返回错误的?br>
 

利用q回值来传播错误D的问题如下:(x)

 

8.      所有函数都必须返回值预留给错误。如果你的函数最自然的语意是q回一个doubleQ而每个double都是有效的。不行,你得把这个返回值通道预留着l错误处理用。你可能?x)说Q我的函数很单,不会(x)出错。但如果以后你修改了(jin)之后Q函数复杂了(jin)呢?到那个时候再把返回的double改ؓ(f)intq加上一个double&作ؓ(f)out参数的话Q改动可大?jin)?br>

9.      q回值所能承载的错误信息是有限的。NULLQ?1Q什么意思?具体的错误信息只能用GetLastError来提?#8230; 哦,对了(jin)Q你看见有多h用过GetLastError的?


10.  不优雅的代码。呃…q个问题前面不是说过?jin)么Q不Q这里说的是另一个不优雅之处——占用了(jin)用来q回l果的返回值通道。本来很自然?#8220;计算——返回结?#8221;Q变成了(jin)“计算——修改out参数——返回错?#8221;。当?dng)你可以说q个问题不是很严重。的,double res = readInput();改ؓ(f)double res; readInput(&res);也没什么大不了(jin)的。如果是q调用呢Q比如,process(readInput());?#8230; 或者readInput() + …Q或者一般地Qop1(op2(), op3(), …);Q?br>

11.  错误汇报Ҏ(gu)不一致性。看看Win32下面的错误汇报机制吧QHRESULT、BOOL、GetLastError…本质上就是因为利用返回值通道是一个补丁方案,错误处理是程序的一个方面(aspectQ,理应有其单独的汇报通道。利用异常的话,错误汇报Ҏ(gu)q即统一?jin),因?f)q是一个first-class的语aU支持机制?br>

12.  有些函数Ҏ(gu)无法q回|如构造函数。有些函数返回值是语言限制好了(jin)的,如重载的操作W和隐式转换函数?br>
 

异常与错误代码的本质区别之三——异常本w能够携带Q意丰富的信息?br>
 

13.  有什么错误报告机制能比错误报告本w就包含量丰富的信息更好的呢?使用异常的话Q你可以往异常c里面添加数据成员,d成员函数Q携带Q意的信息Q比如Java的异常类q省携带了(jin)非常有用的调用栈信息Q。而错误代码就只有一个单薄的数字或字W串Q要携带其它信息只能另外存在其它地方Qƈ期望你能通过GetLastErrorL看?br>
 

异常与错误代码的本质区别之四——异常是OO的?br>
 

14.  你可以设计自q异常l承体系。呃…那这又有什么用呢?当然有用Q一个最大的好处是你可以在L抽象层次上catch一l异常(exception groupingQ,比如你可以用catch(IOException)来捕h有的IO异常Q用catch(SQLException)来捕h有的SQL异常。用catch(FileException)来catch所有的文g异常。你也可以catch更明一点的异常Q如StreamEndException。MQcatch的粒度是_是l,Ҏ(gu)需要,随你调节。当然了(jin)Q你可以设计自己的新异常。能够catch一l相兛_常的好处是你可以很方便的对他们做统一的处理?br>
 

异常与错误代码的本质区别之五——异常是强类型的?br>
 

15.  异常是强cd的。在catch异常的时候,一个特定类型的catch只能catchcd匚w的异常。而用error-code的话Q就跟enum一Pcd不安全?1 == foo()QFAILED == foo()QMB_OK == foo()Q大家反正都是整数?br>
 

异常与错误代码的本质区别之六——异常是first-class的语a机制?br>
 

16.  代码分析工具可以识别出异常ƈq行各种监测或分析。比如PerfMon׃(x)对程序中的异常做l计。这个好处放在未来时态下或许怎么都不应该觑?br>
 

选择什么错误处理机Ӟ

看完上面的比较,{案怿应该已经很明显了(jin)Q异常?br>
 

如果你仍然是error-code的思维?fn)惯的话Q可以假惛_所有error-code的地Ҏ(gu)为抛出exception。需要注意的是,error-code不是status-code。ƈ非所有返回值都是用来报告真正的错误的,有些只不q是控制E序的。就返回的是bool|比如查找子串Q返回是否查扑ֈQ,也ƈ不代表false的情况就是一个错误。具体参加上一节:(x)“哪些情况不属于错?#8221;?br>
 

一个最为广泛的误解是Q异常引入了(jin)不菲的开销Q而error-code没有开销Q所以应该用error-code。这个论点的漏洞在于Q它认ؓ(f)只要是开销是有问题的Q而不兛_(j)到底是在什么情况下的开销。实际上Q现代的~译器早已能够做到异常在happy path上的零开销。当?dng)I间开销q是有的Q因为零开销Ҏ(gu)用的是地址表方案;但相较于旉开销Q这里的I间开销几乎从来都不是个问题。另一斚wQ一旦发生了(jin)异常Q程序肯定就Z(jin)问题Q这个时候的旉开销往往׃那么重要?jin)。此外有Z(x)_(d)那如果频J抛出异常呢Q如果频J抛出异常,往往意味着那个异常对应的ƈ非一个错误情c(din)?br>
 

《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》里面一再强调:(x)不要在项目里面关闭异常支持。因为就你的项目里面不抛出异常Q标准库也依赖于异常。一旦关闭异常,不仅你的目代码都要依赖于error-codeQerror-code的缺点见下一节)(j)Q整个标准库便也都要依赖于非标准的途径来汇报错误,或者干脆就不汇报错误。如果你的项目是如此的硬实时Q乃至于你在非常心(j)且深入的分析之后发觉某些操作真的负担不v异常些微的空间开销和unhappy path上的旉开销的话Q也要尽量别在全局关闭异常支持Q而是量这些敏感的操作集中C个模块中Q按模块关闭异常?br>
 

插曲Q异常的例外情况

凡事都有例外。《C++ Coding Standards: 101 Rules, Guidelines, and Best Practices》上面陈qC(jin)两个例外情况Q一Q用异常没有带来明显的好处的时候:(x)比如所有的错误都会(x)在立卌用端解决掉或者在非常接近立即调用端的地方解决掉。二Q在实际作了(jin)定之后发现异常的抛出和捕获D?jin)显著的旉开销Q这通常只有两种情况Q要么是在内层@环里面,要么是因抛出的异常根本不对应于一个错误?br>
 

如何q行错误处理Q?br>
q个问题同样极其重要。它分ؓ(f)三个子问题:(x)

 

1.      何时抛出异常?br>
2.      何时捕获异常?br>
3.      如何避开异常Q保持异怸立(?#8220;异常透明”Q?br>
 

其中最后一个问题最为重要,属于错误处理的本质性困难之一?br>
 

先说前两个问题?br>
 

从本质上Q错误分ZU,一U是可恢复的Q另一U是不可恢复的?br>
 

对于可恢复的错误。有两种Ҏ(gu)Q?br>

1.      在错误的发生点上立即׃以恢复。比如配|文件不存在便创Z个缺省的Q某个配|项~失׃用缺省值等{。这一Ҏ(gu)的好处是当前函数不返回Q何错误,因ؓ(f)错误被当x定了(jin)Q就像没发生一栗这U方案的前提是当前函数必要有对付该错误的够上下文Q如果一个底层的函数对全局语意没有_的视图,q时可以抛出异常,׃层函数负责恢复?br>

2.      在某个上层栈上恢复。这U情况下Q在负责恢复的那层栈以下的调用一般被看成一个整体事务,其中发生的Q何错误都D整个事务回滚Q回滚到错误恢复栈层面时Q由相应的catch子句q行恢复Qƈ重新执行整个事务Q或者将E序引向另一条备选\径(alternativeQ?br>
 

对于不可恢复的错误。也有两U方案:(x)

 

1.      Sudden Death。在错误的发生点上退出模块(可能伴随着重启模块Q。退出模块前往往需要先释放资源、保存关键数据、记录日志,{等。该Ҏ(gu)的前提是在错误的发生点的上下文中必须要能够释放所有资源,要能够保存关键数据。要满q个前提Q可以用一个全局的沙盒来保存整个模块到当前ؓ(f)止申L(fng)所有资源,从而在M出错点上都可以将q个沙盒整个释放掉。也可以用智能垃圾收集,q样在出错点上只要记录日志和保存数据Q把扫尾工作留给垃圾攉器完成。这个方案的q是如果释放资源是要按某种ơ序的就比较ȝ(ch)?br>

2.      回滚。如果你q没有用垃圾攉Q要到能够回收文件句柄,|络端口{,不光是内存)(j)Q或者你q没有在某个全局可访问的位置保存到当前ؓ(f)止模块申L(fng)所有资源,或者你的资源互怹间有依赖关系Q必L照分配的逆序释放Q等{,那么必L照调用栈的反方向回滚事务。回滚到一个所谓的Fault BarrierQ用一个catch-all在那里等着Q所谓Fault-Barrier的作用就是ؓ(f)?jin)抓q些没法妥善恢复的错误的Q它做的事情通常是logging、发送错误报告、可能也?x)重启模块。Fault Barrier一般在一个内聚的单一职责的功能模块的边界出现?br>
 

严格来说Q其实还有第三种情况Q即~写当前代码的时候ƈ不确定某个错误是否能被恢复。这U时候抛Z个异常往往是最灉|的选择Q因为在没有惛_恢复Ҏ(gu)的时候,上层调用对该异常都是中立的。一旦后面想好恢复方案了(jin)Q不是在某个上层调用内捕获该异常,q是在最底层错误发生点上q卌决错误从而根本取消掉该异常,都没有问题?br>
 

异常转换

在如何抛出和捕获异常的问题上Q还有一个子问题是异常的{换(translationQ。以下情况下应该转换一个由底层传上来的异常Q?br>
 

1.      抛出一个对应于当前抽象层的异常。比如Document::open当接收到底层的文件异常(或数据库异常Q网l异常,取决于这个Document来自何方Q时Q将其{换ؓ(f)“文档无效或被破坏”异常Q增加高层语意,q免暴露底层实现细节(对异常的一个批评就是会(x)暴露内部实现Q而实际上Q通过适当转换异常Q可以得异常M于当前的抽象层次上,成ؓ(f)接口的一部分Q?br>

2.      在模块边界上。如果一个模块,在内部用异常,但在边界上必L供C API的话Q就必须在边界上捕获异常q将其{换ؓ(f)error-code?br>
 

没有旉机器——错误处理的本质困难

刚才提到“回滚”。那么,在异常发生的时候,如何回滚既然已经发生的操作?q就是要说的W三个问题:(x)如何在异总发生点一路传播到捕获点的路径上保持异怸立,卛_滚操作,释放资源。简而言之就是要做到错误安全Qerror-safeQ。错误安全攸兛_异常安全保证和事务语意。在异常~程里面Q错误安全是最重要的一环?br>
 

理想情况下,我们要的是一个时间机器:(x)打碎的杯子要能还原,释放的内存要能重新得刎ͼ销毁的对象像没销毁前一模一P发射的导弹就像从来也没有d发射{一P数据库就像从来没被写入一?#8230;

 

没有旉机器?br>
 

那么Q如何回滚木已成舟的操作Q?br>
 

目前有两个主要方案:(x)

 

1.      DiscardQ丢弃)(j)Q一个例子就能够说明q种做法Q源自STL?#8220;copy-swap手法”。比如一个vectorQ你要往里面插入一个元素,如果插入元素p|的话你想要vectorl持原状Q就好像从来没有动过一栗如何做到这一点呢Q你可以先把q个vector拯一份,往拯里面插入元素Q然后将两个vector调换QswapQ一下即可,swap是不?x)失败的Q因为它只是把两个指针互换了(jin)一下。而如果往那个拯里面插入元素p|的话Q拷贝就?x)被DiscardQ丢弃掉Q,不会(x)带来M实际的副作用。当?dng)q种做法是有代h(hun)的,谁叫你要强异常安全保证呢Q再比如一个拷贝赋值操作符可以q样写:(x)MyClass(other).swap(*this); 当然Q前提还是swap()必须h标准的no-throw语意?br>
q种做法一般化的描q就是:(x)“在一?#8216;副本’里把所有的事情都做好了(jin)Q然后用一个不?x)出错的函数提交QcommitQ?#8221;。这样一来,中途出?jin)Q何错误只要丢弃那个副本即可(往往只要d析构Q。要做到q一点,一个原则就是:(x)“在破坏一份信息之前要保其新的版本一定能够无错的替换掉原信息”Q例如在拯构造函CQ不能先delete再newQ因为new可能p|Q一旦newp|?jin),delete掉的信息可就找不回来?jin)?br>

2.      UndoQ撤销Q:(x)有时候,你一斚w不想付出DiscardҎ(gu)的(通常不菲的)(j)I间开销Q另一斚w你又x有强异常安全保证。这也是有办法的Q比如:(x)

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

scope(failure)是D语言的特性,其语意显而易见:(x)如果当前的scope以失败(异常Q退出的话,{}内的语句p执行。这么一来,在上面的例子中,如果后箋的操作失败,那么q个person׃(x)被从m_persons中pop_back出来Q一ơ事务撤销Q得这个函数对m_persons的媄(jing)响归零?br>
该方案的前提有两个:(x)一Q回滚操作(比如q里的m_persons.pop_back()Q必d在且不会(x)p|Qno-throwQ。比如missile.lunch()׃能回滚,操作pȝAPI一般也无法回滚QI/O操作也无法回滚;另一斚wQ内存操作一般而言都是可以回滚的,只要回复原来内存的值即可。二Q被回滚的操作(比如q里的m_persons.push_back(person)Q也一定要是强异常保证的。比如在中间而不是尾部插入一个personQm_persons.insert(iter, person)Q就不是Z证的Q这U时候就要诉诸前一个方案(DiscardQ?br>
D的scope(failure)Q还有scope(exit)、scope(success)Q是非常强大的设施。利用它Q一个具有事务语意的函数的一般模式如下:(x)

  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)QAndrei Alexandrescu曑֮C(jin)一个ScopeGuardc[11]Q而旨在完全模拟D的scopeҎ(gu)的boost.scope-exit也在review中。只不过C++03里面的模拟方案有一些学?fn)曲U和使用注意点,C++09之后?x)有更方便的?gu)。在其它不支持scope(failure)的语a中,也可以模拟这U做法,不过做法很笨拙?br>

3.      ?#8230; 哪来的第三个Ҏ(gu)Q前面不是说?jin)只有两个方案吗Q是的。因W三个方案是“理想”Ҏ(gu)Q目前还没有q入L语言Q不q在haskell里面已经初见端倪了(jin)。强异常安全保证的核?j)思想其实是事务语意Q而事务语意的核心(j)思想是“不成功便成仁”Q这个思想有许多有的说法Q比如:(x)“武士道原?#8221;?#8220;要么直着回来要么横着回来”?#8220;q不?jin)就L”Q,Ҏ(gu)q个xQ其实最单的Ҏ(gu)是把一l属于同一事务的操作简单地圈v来(标记出来Q,把回滚操作留l语a实现d成:(x)

  stm {
    op1;
    op2;
    …
    opN;
  }

只要op1至opN中Q意一个失败,整个stm块对内存的写操作全被自动抛弃(q要求编译器和运行时的支持,一般是用一个缓冲区或写日志来实玎ͼ(j)Q然后异常被自动抛出q个stm块之外。这个方案的优点是它太优了(jin)Q我们几乎只要关注One True Path卛_Q唯一要做的事情就是用stm{…}圈出代码中需要强保证的代码段。这个方案的~点有两个:(x)一Q它跟第一个方案一P有空间开销Q不q空间开销通常要小一点,因ؓ(f)只要~存特定的写操作。二Q当涉及(qing)到操作系lAPIQI/O{?#8220;外部”操作的时候,底层实现未必能够回滚这些操作了(jin)。另一个理Z的可能性是Q当回滚操作和被回滚操作q严格物理对应Q所谓物理对应就是说Q回滚操作将内存回滚到目标操作发生之前的状态)(j)的时候,底层实现也不知道如何回滚?br>
QSTMQY件事务内存)(j)目前在haskell里面实现?jin),Intel也释Z(jin)C/C++的STM预览版编译器。只不过STM原本的意图是实现锁无关算法的。后者就是另一个话题了(jin)。)(j)

 

RAII

实际上刚才还有一个问题没有说Q那是如何保资源一定会(x)被释放(即便发生异常Q,q在D里面对应的是scope(exit)Q在Java里面对应的是finallyQ在C#   里面对应的是scoped using?br>
 

而言之就是,不管当前作用域以何种方式退出,某某操作Q通常是资源释放)(j)都一定要被执行?br>
 

q个问题的答案其实C++E序员们应该耳熟能详?jin)?x)RAII。RAII是C++最为强大的Ҏ(gu)之一。在C++里面Q局部变量的析构函数刚好满q个语意Q无论当前作用域以何U方式退出,所有局部变量的析构函数都必然会(x)被倒着调用一遍。所以只要将有待释放的资源包装在析构函数里面Q就能够保证它们即便在异常发生的情况下也?x)被释放掉?jin)。ؓ(f)此C++提供?jin)一pd的智能指针:(x)auto_ptr、scoped_ptr、scoped_array… 此外所有的STL容器也都是RAII的。在C++里面模拟D的scope(exit)也是利用的RAII?br>
 

RAII相较于java的finally的好处和C#的scoped using的好处是非常明显的。只要一D代码就高下立判Q?br>
 

  // 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();
    }
  }

 

昄QJava版本的(try-finallyQ最臃肿。C#版本Qscoped usingQ稍微好一些,但using毕竟也不属于E序员关?j)的代码逻辑Q仍然属于代码噪韻I况且如果不连l地甌N个资源的话,使用using׃(x)造成层层嵌套l构?br>
 

如果使用RAII手法来封装StreamReadercȝ话(std::fstream是RAIIcȝ一个范例)(j)Q代码就化ؓ(f)Q?br>
 

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

 

好处是显而易见的。完全不用担?j)资源的释放问题Q代码也变得“as simple as possible”。此外,值得注意的是Q以上代码只是演CZ(jin)最单的情况Q其中需要释攄资源只有一个。其实这个例子ƈ不能最明显地展现出RAII强大的地方,当需要释攄资源有多个的时候,RAII的真正强大之处才被展现出来,一般地_(d)如果一个函Cơ申请N个资源:(x)

 

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

 

那么Q用RAII的话Q代码就像上面这L(fng)单。无Z旉出当前作用域Q所有已l构造初始化?jin)的资源都?x)被析构函数自动释放掉。然而如果用try-finally的话Qf()变成了(jin)Q?br>
 

  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;
    }
  }

 

Z么会(x)q么ȝ(ch)呢,本质上就是因为当执行因异常跛_finally块中Ӟ你ƈ不知道执行流是从#1处?2?#8230;q是#N处蟩q来的,所以你不知道应该释攑֓些资源,只能挨个(g)查各个资源是否已l被甌?jin),如果已申请?jin)便将光放;要能够检查每个资源是否已l被甌?jin),往往p求你要在函数一开始将各个资源的句柄全都初始化为nullQ这h可以通过if(hResN==null)来检查第N个资源是否已l申诗?br>
 

最后,RAII其实是scope(exit)的特DŞ式。但在资源释放方面,RAII有其Ҏ(gu)的优势:(x)如果使用scope(exit)的话Q每个资源分配之后都需要用一个scope(exit)跟在后面保护hQ而如果用RAII的话Q一个资源申请就对应于一个RAII对象的构造,释放工作则被隐藏在对象的析构函数中,从而代码d保持?jin)清爽?br>
 

ȝ

本文讨论?jin)错误处理的原因、时机和Ҏ(gu)。错误处理是~程中极光要的一环;也是最被忽视的一环,Gosling把这个归因于大多数程序员在学校的时候都是做着大作业当~程l习(fn)的,而大作业鲜有老师要求要妥善对待错误的Q大安只要“work on the one true path”卛_Q然而现实世界的软g可必面对各U各L(fng)意外情况Q往往一个程序的错误处理机制在第一ơ面寚w误的时候便崩溃?#8230; 另一斚wQ错误处理又是一个极其困隄问题Q其本质困难来源于两个方面:(x)一Q哪些情늮是错误。二Q如何做到错误安全(error-safeQ。相较之下在什么地点解决错误倒是Ҏ(gu)一些了(jin)。本文对错误处理的问题作?jin)详l的分析Qȝ?jin)多q来q个领域争论的结果,提供?jin)一个实践导引?img src ="http://www.shnenglu.com/alexhappy/aggbug/93906.html" width = "1" height = "1" />

alexhappy 2009-08-20 13:35 发表评论
]]>
9999Ʒŷþþþþ| Ժձһձþ | Ʒvaþþþþþ| þþþþþþþþ| ޹ƷþþþþԻ| һƷþþ޹| aëƬþ| ٸ޾þþþþ| Vþþ| Aݺݾþɫ| 99þerֻоƷ18| Vþþ| þ99Ļþ| Ʒvaþþþþþ| þݺҹҹ2020| AVһþ| ˾þô߽Ʒ| þֻǾƷ23| ҹþþþþþþõӰ| þó97˰| 91޹˾þþƷ| ɫʹþۺ| ޹ŷۺ997þ| 97þۺϾƷþþۺ| 91þþƷ91ɫҲ| ޾ƷۺϾþһ| ľþþþר| Ʒþ| ɫۺϾþþĻ| ۺϾþþƷ| 777ҹƷþav| þݹƷһ| þҹ³˿Ƭϼ| þùƷһ| 91鶹Ʒ91þþ| þѹƷһ| Ʒľþþþþþ| 91ƷۺϾþĻþһ | þùһƬѹۿ| þĻƷѩ | þݺҹҹվ|