本文介绍了您(真的)编写异常安全代码吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Exception handling (EH) seems to be the current standard, and by searching the web, I can not find any novel ideas or methods that try to improve or replace it (well, some variations exist, but nothing novel).

尽管大多数人似乎忽略它或只是接受它,但 EH 一些巨大的缺点:异常对代码是不可见的,它会创建很多很多可能的退出点.Joel on software 写了一篇关于它的文章.与 goto 的比较非常合适,这让我再次想到了 EH.

Though most people seem to ignore it or just accept it, EH has some huge drawbacks: exceptions are invisible to the code and it creates many, many possible exit points. Joel on software wrote an article about it. The comparison to goto fits perfect, it made me think again about EH.

我尽量避免使用 EH,只使用返回值、回调或任何适合目的的东西.但是当你必须编写可靠的代码时,现在你不能忽略 EH:它以 new 开头,它可能会抛出异常,而不是仅仅返回 0(就像过去一样).这使得任何 C++ 代码行易受攻击 异常.然后 C++ 基础代码中更多的地方抛出异常...... std lib 会这样做,等等.

I try to avoid EH and just use return values, callbacks or whatever fits the purpose. But when you have to write reliable code, you just can't ignore EH these days: It starts with the new, which may throw an exception, instead of just returning 0 (like in the old days). This makes about any line of C++ code vulnerable to an exception. And then more places in the C++ foundational code throw exceptions... std lib does it, and so on.

这感觉就像在摇摇晃晃的地面上行走..所以,现在我们不得不注意例外情况!

This feels like walking on shaky grounds.. So, now we are forced to take care about exceptions!

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

But its hard, its really hard. You have to learn to write exception safe code, and even if you have some experience with it, it will still be required to double check any single line of code to be safe! Or you start to put try/catch blocks everywhere, which clutters the code until it reaches a state of unreadability.

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

EH replaced the old clean deterministical approach (return values..), which had just a few but understandable and easily solveable drawbacks with an approach that creates many possible exit points in your code, and if you start writing code that catches exceptions (what you are forced to do at some point), then it even creates a multitude of paths through your code (code in the catch blocks, think about a server program where you need logging facilities other than std::cerr ..). EH has advantages, but that's not the point.

我的实际问题:

  • 您真的会编写异常安全代码吗?
  • 您确定您最后的生产就绪"代码是异常安全的吗?
  • 你能确定是吗?
  • 您知道和/或实际使用过有效的替代方法吗?

推荐答案

您的问题断言,编写异常安全的代码非常困难".我会先回答你的问题,然后再回答背后隐藏的问题.

Your question makes an assertion, that "Writing exception-safe code is very hard". I will answer your questions first, and then, answer the hidden question behind them.

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

当然,我愿意.

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

This is the reason Java lost a lot of its appeal to me as a C++ programmer (lack of RAII semantics), but I am digressing: This is a C++ question.

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

It is, in fact, necessary when you need to work with STL or Boost code. For example, C++ threads (boost::thread or std::thread) will throw an exception to exit gracefully.

您确定您的最后一次生产准备就绪"吗?代码是异常安全的吗?

你能确定吗?

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

Writing exception-safe code is like writing bug-free code.

您不能 100% 确定您的代码是异常安全的.但随后,您会为之奋斗,使用众所周知的模式,并避免众所周知的反模式.

You can't be 100% sure your code is exception safe. But then, you strive for it, using well-known patterns, and avoiding well-known anti-patterns.

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

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

There are no viable alternatives in C++ (i.e. you'll need to revert back to C and avoid C++ libraries, as well as external surprises like Windows SEH).

要编写异常安全代码,您必须首先了解您编写的每条指令的异常安全级别.

To write exception safe code, you must know first what level of exception safety each instruction you write is.

例如,new 可以抛出异常,但分配内置函数(例如 int 或指针)不会失败.交换永远不会失败(永远不要写抛出交换),std::list::push_back 可以抛出...

For example, a new can throw an exception, but assigning a built-in (e.g. an int, or a pointer) won't fail. A swap will never fail (don't ever write a throwing swap), a std::list::push_back can throw...

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

The first thing to understand is that you must be able to evaluate the exception guarantee offered by all of your functions:

  1. none:你的代码不应该提供这个.此代码将泄漏所有内容,并在第一个抛出异常时崩溃.
  2. 基本:这是您至少必须提供的保证,也就是说,如果抛出异常,则不会泄漏任何资源,并且所有对象仍然是完整的
  3. strong:处理要么成功,要么抛出异常,但如果抛出异常,则数据将处于与处理根本没有开始时相同的状态(这给出了一个C++ 的事务处理能力)
  4. nothrow/nofail:处理会成功.
  1. none: Your code should never offer that. This code will leak everything, and break down at the very first exception thrown.
  2. basic: This is the guarantee you must at the very least offer, that is, if an exception is thrown, no resources are leaked, and all objects are still whole
  3. strong: The processing will either succeed, or throw an exception, but if it throws, then the data will be in the same state as if the processing had not started at all (this gives a transactional power to C++)
  4. nothrow/nofail: The processing will succeed.

代码示例

下面的代码看起来像正确的 C++,但实际上,提供了无"的代码.保证,因此,它是不正确的:

Example of code

The following code seems like correct C++, but in truth, offers the "none" guarantee, and thus, it is not correct:

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
}

我在编写所有代码时都考虑到了这种分析.

I write all my code with this kind of analysis in mind.

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

The lowest guarantee offered is basic, but then, the ordering of each instruction makes the whole function "none", because if 3. throws, x will leak.

首先要做的是将函数设为基本"函数,即将 x 放入智能指针中,直到它被列表安全拥有:

The first thing to do would be to make the function "basic", that is putting x in a smart pointer until it is safely owned by the list:

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
}

现在,我们的代码提供了一个基本的"保证.什么都不会泄漏,所有对象都将处于正确状态.但我们可以提供更多,即强有力的保证.这就是它可能变得昂贵的地方,这就是为什么并非所有 C++代码都很强大.让我们试试吧:

Now, our code offers a "basic" guarantee. Nothing will leak, and all objects will be in a correct state. But we could offer more, that is, the strong guarantee. This is where it can become costly, and this is why not all C++ code is strong. Let's try it:

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 可以被认为是强":如果有东西抛出,则 t 不会被修改,并且 X 不会泄漏,因为它属于智能指针.

We re-ordered the operations, first creating and setting X to its right value. If any operation fails, then t is not modified, so, operation 1 to 3 can be considered "strong": If something throws, t is not modified, and X will not leak because it's owned by the smart pointer.

然后,我们创建 t 的副本 t2,并从操作 4 到操作 7 处理此副本.如果发生异常,t2被修改了,但是 t 仍然是原始的.我们仍然提供强有力的保证.

Then, we create a copy t2 of t, and work on this copy from operation 4 to 7. If something throws, t2 is modified, but then, t is still the original. We still offer the strong guarantee.

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

Then, we swap t and t2. Swap operations should be nothrow in C++, so let's hope the swap you wrote for T is nothrow (if it isn't, rewrite it so it is nothrow).

所以,如果我们到达函数的末尾,一切都会成功(不需要返回类型)并且 t 有它的异常值.如果失败,那么 t 仍然是它的原始值.

So, if we reach the end of the function, everything succeeded (No need of a return type) and t has its excepted value. If it fails, then t has still its original value.

现在,提供强保证的成本可能相当高,所以不要力求为所有代码提供强保证,但如果你可以免费提供(并且 C++ 内联和其他优化可以使所有代码以上无成本),然后去做.函数用户会感谢你的.

Now, offering the strong guarantee could be quite costly, so don't strive to offer the strong guarantee to all your code, but if you can do it without a cost (and C++ inlining and other optimization could make all the code above costless), then do it. The function user will thank you for it.

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

It takes some habit to write exception-safe code. You'll need to evaluate the guarantee offered by each instruction you'll use, and then, you'll need to evaluate the guarantee offered by a list of instructions.

当然,C++ 编译器不会支持保证(在我的代码中,我将保证作为@warning doxygen 标记提供),这有点可悲,但它不应该阻止您尝试编写异常-安全代码.

Of course, the C++ compiler won't back up the guarantee (in my code, I offer the guarantee as a @warning doxygen tag), which is kinda sad, but it should not stop you from trying to write exception-safe code.

程序员如何保证无失败函数总是成功?毕竟,该函数可能有错误.

How can a programmer guarantee that a no-fail function will always succeed? After all, the function could have a bug.

这是真的.异常保证应该由无错误的代码提供.但是,在任何语言中,调用函数都假定该函数没有错误.没有健全的代码可以保护自己免受错误的可能性.尽你所能编写代码,然后假设它没有错误,提供保证.如果有错误,请纠正它.

This is true. The exception guarantees are supposed to be offered by bug-free code. But then, in any language, calling a function supposes the function is bug-free. No sane code protects itself against the possibility of it having a bug. Write code the best you can, and then, offer the guarantee with the supposition it is bug-free. And if there is a bug, correct it.

异常是针对异常处理失败,而不是针对代码错误.

Exceptions are for exceptional processing failure, not for code bugs.

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

Now, the question is "Is this worth it ?".

当然可以.有一个不投/不失败";知道函数不会失败的函数是一个很大的福音.对于强"牌也可以这样说.函数,它使您能够编写具有事务语义的代码,例如数据库,具有提交/回滚功能,提交是代码的正常执行,抛出异常是回滚.

Of course, it is. Having a "nothrow/no-fail" function knowing that the function won't fail is a great boon. The same can be said for a "strong" function, which enables you to write code with transactional semantics, like databases, with commit/rollback features, the commit being the normal execution of the code, throwing exceptions being the rollback.

然后,基本"是您应该提供的最低限度的保证.C++ 在那里是一门非常强大的语言,它的作用域使您能够避免任何资源泄漏(垃圾收集器很难为数据库、连接或文件句柄提供这种功能).

Then, the "basic" is the very least guarantee you should offer. C++ is a very strong language there, with its scopes, enabling you to avoid any resource leaks (something a garbage collector would find it difficult to offer for the database, connection or file handles).

所以,在我看来,它值得的.

So, as far as I see it, it is worth it.

nobar 发表了我认为非常相关的评论,因为它是如何编写异常安全代码"的一部分:

nobar made a comment that I believe, is quite relevant, because it is part of "how do you write exception safe code":

  • [me] 交换永远不会失败(甚至不要写投掷交换)
  • [nobar] 对于自定义编写的 swap() 函数,这是一个很好的建议.然而,应该注意的是,std::swap() 可能会根据它内部使用的操作而失败
  • [me] A swap will never fail (don't even write a throwing swap)
  • [nobar] This is a good recommendation for custom-written swap() functions. It should be noted, however, that std::swap() can fail based on the operations that it uses internally

默认的 std::swap 将制作副本和赋值,对于某些对象,它们可以抛出.因此,默认交换可能会抛出,用于您的类,甚至用于 STL 类.就 C++ 标准而言,vectordequelist 的交换操作不会抛出,而对于 map 如果比较函子可以引发复制构造(参见 C++ 编程语言,特别版,附录 E,E.4.3.Swap).

the default std::swap will make copies and assignments, which, for some objects, can throw. Thus, the default swap could throw, either used for your classes or even for STL classes. As far as the C++ standard is concerned, the swap operation for vector, deque, and list won't throw, whereas it could for map if the comparison functor can throw on copy construction (See The C++ Programming Language, Special Edition, appendix E, E.4.3.Swap).

查看向量交换的 Visual C++ 2008 实现,如果两个向量具有相同的分配器(即正常情况),则向量的交换不会抛出,但如果它们具有不同的分配器,则会生成副本.因此,我认为它可能会在最后一种情况下抛出.

Looking at Visual C++ 2008 implementation of the vector's swap, the vector's swap won't throw if the two vectors have the same allocator (i.e., the normal case), but will make copies if they have different allocators. And thus, I assume it could throw in this last case.

所以,原文仍然成立:永远不要写投掷交换,但必须记住 nobar 的评论:确保你正在交换的对象具有非投掷交换.

So, the original text still holds: Don't ever write a throwing swap, but nobar's comment must be remembered: Be sure the objects you're swapping have a non-throwing swap.

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

Dave Abrahams, who gave us the basic/strong/nothrow guarantees, described in an article his experience about making the STL exception safe:

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

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

Look at the 7th point (Automated testing for exception-safety), where he relies on automated unit testing to make sure every case is tested. I guess this part is an excellent answer to the question author's "Can you even be sure, that it is?".

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

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

Dionadar is referring to the following line, which indeed has undefined behaviour.

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

这里的解决方案是在进行加法之前验证整数是否已经达到最大值(使用 std::numeric_limits<T>::max()).

The solution here is to verify if the integer is already at its max value (using std::numeric_limits<T>::max()) before doing the addition.

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

My error would go in the "Normal failure vs. bug" section, that is, a bug.It doesn't invalidate the reasoning, and it does not mean the exception-safe code is useless because impossible to attain.You can't protect yourself against the computer switching off, or compiler bugs, or even your bugs, or other errors. You can't attain perfection, but you can try to get as near as possible.

我根据 Dionadar 的评论更正了代码.

I corrected the code with Dionadar's comment in mind.

这篇关于您(真的)编写异常安全代码吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-06 03:34