首页 文章

你(真的)编写异常安全代码吗? [关闭]

提问于
浏览
296

异常处理(EH)似乎是当前的标准,并且通过搜索网络,我找不到任何试图改进或替换它的新颖想法或方法(好吧,存在一些变化,但没有新颖的) .

虽然大多数人似乎忽略它或只是接受它,但是EH has 有一些巨大的缺点:代码看不到异常,它会创建许多可能的退出点 . 乔尔在软件上写了一篇article about it . 与 goto 的比较非常完美,它让我再次想到了EH .

我尽量避免使用EH,只使用返回值,回调或任何适合目的的东西 . 但是 when you have to write reliable code, you just can't ignore EH these days :它以 new 开头,可能会抛出一个异常,而不是只返回0(就像过去一样) . 这使得任何C代码行都容易受到异常的影响 . 然后C基础代码中的更多位置抛出异常...... std lib执行它,依此类推 .

感觉像是 walking on shaky grounds ..所以,现在我们被迫关注例外!

但它很难,真的很难 . 你必须学会编写异常安全代码,即使你有一些经验,它仍然需要仔细检查任何一行代码是安全的!或者你开始在任何地方放置try / catch块,这会使代码混乱,直到它达到不可读状态 .

EH取代了旧的干净确定性方法(返回值..),它只有一些但可以理解且易于解决的缺点,在您的代码中创建许多可能的退出点的方法,以及如果您开始编写捕获异常的代码(您是什么的)在某些时候被迫做某事),然后它甚至通过你的代码创建了许多路径(catch块中的代码,考虑一个服务器程序,你需要除了std :: cerr之外的日志工具..) . EH有优势,但这不是重点 .

我的实际问题:

  • 你真的写异常安全代码吗?

  • 您确定最后的"production ready"代码是异常安全的吗?

  • 你能确定吗,它是吗?

  • 您知道和/或实际使用有效的替代品吗?

13 回答

  • 2

    在"any line can throw"的假设下,不可能编写异常安全的代码 . 异常安全代码的设计主要依赖于您应该在代码中期望,观察,遵循和实现的某些 Contract /保证 . 保证永不抛出的代码是绝对必要的 . 还有其他种类的例外保证 .

    换句话说,创建异常安全代码在很大程度上是程序设计的问题,而不仅仅是普通编码问题 .

  • 7

    在C中编写异常安全的代码与使用大量try {} catch {}块无关 . 它是关于记录代码提供什么样的保证 .

    我建议阅读Herb Sutter的Guru Of The Week系列,特别是分期59,60和61 .

    总而言之,您可以提供三种级别的异常安全性:

    • Basic:当您的代码抛出异常时,您的代码不会泄漏资源,并且对象仍然是可破坏的 .

    • Strong:当您的代码抛出异常时,它会保持应用程序状态不变 .

    • 不抛:您的代码永远不会抛出异常 .

    就个人而言,我发现这些文章的时间很晚,因此我的C代码绝对不是例外 .

  • 5

    我们中的一些人已经使用例外超过20年 . 例如,PL / I有它们 . 它们是一种新的危险技术的前提似乎对我来说是个问题 .

  • 2

    首先(正如Neil所说),SEH是微软的结构化异常处理 . 它与C中的异常处理类似但不完全相同 . 实际上,如果你想在Visual Studio中使用它,你必须enable C++ Exception Handling - 默认行为并不能保证在所有情况下都会销毁本地对象!在任何一种情况下,异常处理并不是真的更难,它只是不同 .

    现在为您提出实际问题 .

    你真的写异常安全代码吗?

    是 . 在所有情况下,我都努力寻求异常安全代码 . 我使用RAII技术宣传资源的范围访问(例如,boost::shared_ptr用于内存,boost::lock_guard用于锁定) . 通常,RAIIscope guarding技术的一致使用将使异常安全代码更容易编写 . 诀窍是了解存在的内容以及如何应用它 .

    您确定最后一个“ 生产环境 就绪”代码是异常安全的吗?

    不,这是安全的 . 我可以说我没有期待完美的代码,只需要编写良好的代码 . 除了提供异常安全性之外,上述技术还可以通过 try / catch 块几乎无法实现的方式保证正确性 . 如果您正在捕获最高控制范围(线程,进程等)中的所有内容,那么您可以确保在异常(most of the time)面前继续运行 . 相同的技术也可以帮助您在异常 without try/catch blocks everywhere 面前继续正常运行 .

    你能确定它是吗?

    是 . 您可以通过彻底的代码审核来确定,但没有人真正做到这一点吗?常规代码评论和细心的开发人员走了很长的路要走到那里 .

    您知道和/或实际使用有效的替代品吗?

    多年来我尝试了一些变化,例如在高位编码状态(ala HRESULTs)或者可怕的setjmp() ... longjmp() hack . 这两种方式在实践中都以完全不同的方式分解 .


    最后,如果您习惯于应用一些技巧并仔细考虑在异常情况下实际可以执行某些操作的位置,那么最终会得到非常易读且异常安全的代码 . 您可以按照以下规则进行总结:

    • 您只想查看 try / catch ,当您可以对某个特定异常采取措施时

    • 你几乎不想在代码中看到原始的 newdelete

    • Eschew std::sprintfsnprintf 和一般数组 - 使用 std::ostringstream 格式化并用 std::vectorstd::string 替换数组

    • 如有疑问,请在推出自己的功能之前在Boost或STL中查找功能

    我只能建议您学习如何正确使用异常,如果计划用C语言编写,请忘记结果代码 . 如果要避免异常,可能需要考虑使用does not have themmakes them safe的另一种语言编写 . 如果您想真正学习如何充分利用C,请阅读Herb SutterNicolai JosuttisScott Meyers中的几本书 .

  • 17

    你的问题断言,“编写异常安全的代码非常困难” . 我先回答你的问题,然后回答他们背后隐藏的问题 .

    回答问题

    你真的写异常安全代码吗?

    我当然是了 .

    这是因为Java作为C程序员(缺乏RAII语义)而失去了很多吸引力,但我很离题:这是一个C问题 .

    事实上,当您需要使用STL或Boost代码时,这是必要的 . 例如,C线程( boost::threadstd::thread )将抛出异常以正常退出 .

    您确定最后一个“ 生产环境 就绪”代码是异常安全的吗?你能确定吗,它是吗?

    编写异常安全的代码就像编写无错误的代码一样 .

    您不能100%确定您的代码是异常安全的 . 但是,然后,你使用众所周知的模式,并避免众所周知的反模式来争取它 .

    您知道和/或实际使用有效的替代品吗?

    在C中没有可行的替代方案(即,你需要恢复到C并避免使用C库,以及像Windows SEH这样的外部意外) .

    编写异常安全代码

    要编写异常安全代码,您必须知道 first 您编写的每条指令的异常安全级别 .

    例如,一个 new 可以抛出一个异常,但是分配一个内置的(例如一个int或一个指针)赢了't fail. A swap will never fail (don' t写了一个投掷交换), std::list::push_back 可以抛出......

    例外保证

    首先要理解的是,您必须能够评估所有功能提供的异常保证:

    • none :您的代码永远不应该提供 . 此代码将泄漏所有内容,并在抛出的第一个异常时分解 .

    • basic :这是您必须至少提供的保证,也就是说,如果抛出异常,没有资源泄露,并且所有对象仍然是完整的

    • strong :处理将成功,或者抛出异常,但是如果它抛出,那么数据将处于相同的状态,就好像处理根本没有开始一样(这给了C的事务处理能力)

    • nothrow/nofail :处理成功 .

    代码示例

    以下代码看起来像是正确的C,但事实上,它提供了“无”保证,因此,它不正确:

    void doSomething(T & t)
    {
       if(std::numeric_limits<int>::max() > t.integer)  // 1.   nothrow/nofail
          t.integer += 1 ;                              // 1'.  nothrow/nofail
       X * x = new X() ;                // 2. basic : can throw with new and X constructor
       t.list.push_back(x) ;            // 3. strong : can throw
       x->doSomethingThatCanThrow() ;   // 4. basic : can throw
    }
    

    我用这种方式编写了所有代码分析记 .

    提供的最低保证是基本的,但是,每条指令的顺序使整个函数“无”,因为如果3.抛出,x将泄漏 .

    要做的第一件事就是使函数“基本”,即将x放入智能指针,直到它被列表安全拥有:

    void doSomething(T & t)
    {
       if(std::numeric_limits<int>::max() > t.integer)  // 1.   nothrow/nofail
          t.integer += 1 ;                              // 1'.  nothrow/nofail
       std::auto_ptr<X> x(new X()) ;    // 2.  basic : can throw with new and X constructor
       X * px = x.get() ;               // 2'. nothrow/nofail
       t.list.push_back(px) ;           // 3.  strong : can throw
       x.release() ;                    // 3'. nothrow/nofail
       px->doSomethingThatCanThrow() ;  // 4.  basic : can throw
    }
    

    现在,我们的代码提供了"basic"保证 . 没有任何东西会泄漏,所有物体都将处于正确的状态 . 但我们可以提供更多,即强有力的保证 . 这是它可能变得昂贵的地方,这就是为什么 not all C代码很强大 . 我们来试试吧:

    void doSomething(T & t)
    {
       // we create "x"
       std::auto_ptr<X> x(new X()) ;    // 1. basic : can throw with new and X constructor
       X * px = x.get() ;               // 2. nothrow/nofail
       px->doSomethingThatCanThrow() ;  // 3. basic : can throw
    
       // we copy the original container to avoid changing it
       T t2(t) ;                        // 4. strong : can throw with T copy-constructor
    
       // we put "x" in the copied container
       t2.list.push_back(px) ;          // 5. strong : can throw
       x.release() ;                    // 6. nothrow/nofail
       if(std::numeric_limits<int>::max() > t2.integer)  // 7.   nothrow/nofail
          t2.integer += 1 ;                              // 7'.  nothrow/nofail
    
       // we swap both containers
       t.swap(t2) ;                     // 8. nothrow/nofail
    }
    

    我们重新订购了操作,首先创建并将 X 设置为正确的值 . 如果任何操作失败,则 t 不会被修改,因此,操作1到3可以被认为是"strong":如果抛出某些内容, t 未被修改,并且 X 将不会泄漏,因为它由智能指针拥有 .

    然后,我们创建 tt 副本,并从操作4到7处理此副本 . 如果抛出某些内容, t2 被修改,但是, t 仍然是原始的 . 我们仍然提供强有力的保证 .

    然后,我们交换 tt2 . 交换操作应该不在C中,所以让我们希望你为 T 写的交换是不是(如果不是,重写它以便它不是) .

    因此,如果我们到达函数的末尾,一切都成功(不需要返回类型), t 具有其例外值 . 如果失败,则 t 仍然是其原始值 .

    现在,提供强有力的保证可能是相当昂贵的,所以不要努力为所有代码提供强有力的保证,但如果你可以不花费成本(C内联和其他优化可以使所有代码无成本) ,然后去做 . 功能用户会感谢你 .

    结论

    编写异常安全的代码需要一些习惯 . 您需要评估您将使用的每条指令所提供的保证,然后,您需要评估指令列表提供的保证 .

    当然,C编译器不会备份保证(在我的代码中,我提供了作为@warning doxygen标签的保证),这有点令人遗憾,但它不应该阻止您尝试编写异常安全的代码 .

    正常失败与错误

    程序员如何保证无故障功能永远成功?毕竟,该功能可能有一个错误 .

    这是真的 . 异常保证应该由无错误的代码提供 . 但是,在任何语言中,调用函数都假定函数没有错误 . 没有合理的代码可以保护自己不受错误的影响 . 尽可能地编写代码,然后提供保证,假设它没有错误 . 如果有错误,请更正错误 .

    例外情况是异常处理失败,而不是代码错误 .

    最后的话

    现在,问题是“这值得吗?” .

    当然如此 . 知道函数不会失败的“nothrow / no-fail”函数是一个很大的好处 . 对于“强”函数也是如此,它使您能够使用事务语义编写代码,如数据库,具有提交/回滚功能,提交是代码的正常执行,抛出异常是回滚 .

    那么,“基本”是您应该提供的最低保证 . C是一种非常强大的语言,具有其范围,使您能够避免任何资源泄漏(垃圾收集器会发现很难为数据库,连接或文件句柄提供) .

    所以,据我所知,它是值得的.1401481_值得 .

    编辑2010-01-29:关于非投掷交换

    nobar做了一个我相信的评论是非常相关的,因为它是“你如何编写异常安全代码”的一部分:

    • [me]交换永远不会失败(甚至不写掉投掷交换)

    • [nobar]对于自定义编写的 swap() 函数,这是一个很好的建议 . 但应注意, std::swap() 可能会因内部使用的操作而失败

    默认 std::swap 将进行复制和分配,对于某些对象,可以抛出 . 因此,默认交换可以抛出,既可用于您的类,也可用于STL类 . 就C标准而言, vectordequelist 的交换操作不会抛出,而如果比较函数可以抛出复制结构,则可以抛出 map (参见C编程语言,特别版,附录E) ,E.4.3.Swap) .

    看着在向量交换的Visual C 2008实现中,如果两个向量具有相同的分配器(即正常情况),则向量的交换将不会抛出,但如果它们具有不同的分配器则将进行复制 . 因此,我认为它可以抛弃最后一种情况 .

    所以,原始文本仍然存在:不要写一个抛出交换,但必须记住nobar的注释:确保你交换的对象有一个非投掷交换 .

    编辑2011-11-06:有趣的文章

    Dave Abrahams,谁给了我们basic/strong/nothrow guarantees,在一篇文章中描述了他关于使STL异常安全的经验:

    http://www.boost.org/community/exception_safety.html

    请看第7点(异常安全的自动化测试),他依靠自动化单元测试来确保每个案例都经过测试 . 我想这部分是对作者的问题的一个很好的答案:“你能确定,它是什么?” .

    编辑2013-05-31:来自dionadar的评论

    t.integer = 1;是不能保证不会发生溢出而不是异常安全,事实上可能在技术上调用UB! (有符号溢出是UB:C 11 5/4“如果在求值表达式期间,结果未在数学上定义或未在其类型的可表示值范围内,则行为未定义 . ”)注意无符号整数不溢出,而是在模数为2 ^#位的等价类中进行计算 .

    Dionadar指的是以下行,它确实具有未定义的行为 .

    t.integer += 1 ;                 // 1. nothrow/nofail
    

    这里的解决方案是在执行添加之前验证整数是否已经处于其最大值(使用 std::numeric_limits<T>::max() ) .

    我的错误将出现在“正常失败与错误”部分,即一个错误 . 它不会使推理无效,并不意味着异常安全的代码是无用的,因为无法实现 . 您无法保护自己免受计算机关闭,编译器错误,甚至您的错误或其他错误 . 你无法达到完美,但你可以尝试尽可能接近 .

    我用Dionadar的评论更正了代码 .

  • 31
    • 你真的写异常安全代码吗?

    好吧,我当然打算 .

    • 你确定你的最后一个"production ready"代码是异常安全的吗?

    我确信使用异常构建的24/7服务器可以全天候运行并且不会泄漏内存 .

    • 你能确定吗,它是吗?

    很难确定任何代码是否正确 . 通常,人们只能按结果去做

    • 您知道和/或实际使用有效的替代品吗?

    没有 . 使用例外比我在编程过去30年中使用的任何替代方案更清晰,更容易 .

  • 16

    抛开SEH和C异常之间的混淆,您需要意识到可以随时抛出异常,并在编写代码时考虑到这一点 . 对异常安全的需求在很大程度上推动了RAII,智能指针和其他现代C技术的使用 .

    如果你遵循完善的模式,编写异常安全的代码并不是特别困难,事实上它比编写在所有情况下都能正确处理错误返回的代码更容易 .

  • 9

    一般来说,EH很好 . 但C的实现并不是非常友好,因为很难说你的异常捕获覆盖有多好 . 例如,Java使这很容易,如果你不处理可能的异常,编译器往往会失败 .

  • 499

    我非常喜欢使用Eclipse和Java(Java新手),因为如果你缺少一个EH处理程序,它会在编辑器中抛出错误 . 这使得忘记处理异常变得更加困难......

    此外,使用IDE工具,它会自动添加try / catch块或其他catch块 .

  • 2

    我们中的一些人更喜欢像Java这样的语言,它迫使我们声明方法抛出的所有异常,而不是像C和C#那样使它们不可见 .

    如果正确完成,异常优于错误返回代码,如果没有其他原因,您不必手动向上传播失败的调用链 .

    话虽这么说,低级API库编程应该可以避免异常处理,并坚持错误返回代码 .

    它很难在C中编写干净的异常处理代码 . 我最终使用了 new(nothrow) .

  • 0

    我尝试了最好的编写异常安全的代码,是的 .

    这意味着我要注意哪些线条可以投掷 . 不是每个人都可以,并且牢记这一点至关重要 . 关键是要考虑并设计您的代码以满足标准中定义的异常保证 .

    可以编写此操作以提供强大的功能例外保证?我必须满足于基本的吗?哪些行可能会抛出异常,如何确保它们不会破坏对象?

  • 2
    • 你真的写异常安全代码吗? [没有这样的事情 . 除非您拥有托管环境,否则例外是对错误的纸张屏障 . 这适用于前三个问题 . ]

    • 您知道和/或实际使用有效的替代品吗? [替代什么?这里的问题是人们不会将实际错误与正常程序操作分开 . 如果是正常的程序操作(即找不到文件),则不是真正的错误处理 . 如果是实际错误,则无法“处理”它或者它不是实际错误 . 你的目标是找出问题所在,并停止电子表格并记录错误,将驱动程序重新启动到烤面包机,或者只是祈祷喷气式战斗机可以继续飞行,即使它的软件是错误的并且希望最好 .

  • 4

    人们做了很多(我甚至会说最多) .

    对于异常而言,真正重要的是,如果您不编写任何处理代码 - 结果非常安全且表现良好 . 太急于恐慌,但安全 .

    你需要在处理程序中积极地犯错以获得不安全的东西,并且只有catch(...){}将与忽略错误代码进行比较 .

相关问题