异常安全——何时、如何、为什么?
我只是一个初出茅庐的程序员,至少尝试编写比最佳情况更多的内容。到目前为止,我一直在阅读 Herb Sutter 的《Exceptional C++》,并浏览了三次异常安全章节。然而,除了他提出的例子(堆栈)之外,我不太确定什么时候应该努力追求异常安全性和速度,什么时候这样做是愚蠢的。
例如,我当前的作业项目是一个双向链表。由于我已经编写了其中的一些程序,因此我想花时间了解一些更深层次的概念,例如 ES。
这是我的 pop-front 函数:
void List::pop_front()
{
if(!head_)
throw std::length_error("Pop front: List is empty.\n");
else
{
ListElem *temp = head_;
head_ = head_->next;
head_->prev = 0;
delete temp;
--size_;
}
}
我对此遇到了一些困境。
1)当列表失败时我真的应该抛出错误吗?难道我不应该简单地什么也不做并返回,而不是强迫列表的用户执行 try {] catch() {} 语句(这也很慢)。
2)有多个错误类(加上我的老师要求我们在类中实现的ListException)。对于这样的事情,自定义错误类真的有必要吗?是否有关于何时使用特定异常类的一般指南? (例如,范围、长度和边界听起来都很相似)
3)我知道在所有引发异常的代码完成之前我不应该更改程序状态。这就是为什么我最后要减少 size_ 。在这个简单的例子中这真的有必要吗?我知道删除不能扔。当分配给 0 时, head_->prev 是否有可能抛出异常? (head 是第一个 Node)
我的 Push_back 函数:
void List::push_back(const T& data)
{
if(!tail_)
{
tail_ = new ListElem(data, 0, 0);
head_ = tail_;
}
else
{
tail_->next = new ListElem(data, 0, tail_);
tail_ = tail_->next;
}
++size_;
}
1) 我经常听说 C++ 程序中任何事情都可能失败。测试 ListElem 的构造函数是否失败(或 new 期间的 tail_ 失败)是否现实?
2)是否有必要测试数据类型(目前是一个简单的 typedef int T 直到我将所有内容模板化)以确保该类型对于结构来说是可行的?
我意识到这些都是过于简单的例子,但我目前只是很困惑什么时候应该真正练习良好的 ES,什么时候不应该。
I'm just a fledgling programmer that at least tries to program more than the best-case scenario. I've been reading Herb Sutter's "Exceptional C++" and went through the exception-safety chapters thrice so far. However, barring the example he posed (a Stack), I'm not really sure when exactly I should strive for exception safety vs speed and when it's just plain silly to do so.
For example, my current homework project is a doubly-linked list. Since I've programmed a couple of these already, I wanted to take the time to get into some deeper concepts such as ES.
Here is my pop-front function:
void List::pop_front()
{
if(!head_)
throw std::length_error("Pop front: List is empty.\n");
else
{
ListElem *temp = head_;
head_ = head_->next;
head_->prev = 0;
delete temp;
--size_;
}
}
I had some dilemmas with this.
1) Should I really throw an error when a list fails? Shouldn't I rather simply do nothing and return instead of forcing the user of the list to perform try {] catch() {} statements (that are also slow).
2) There are multiple error classes (plus the ListException my teacher demands we implement in the class). Is a custom error class really necessary for such a thing, and is there a general guide on when to use a specific exception class? (For example, range, length and boundary all sound alike)
3) I know I shouldn't change the program state until all that code that has thrown an exception be done. This is why I'm decrementing size_ last. Is this really necessary in this simple example? I know delete can't throw. Is it possible for head_->prev to ever throw when assigning to 0? (head is the first Node)
My push_back function:
void List::push_back(const T& data)
{
if(!tail_)
{
tail_ = new ListElem(data, 0, 0);
head_ = tail_;
}
else
{
tail_->next = new ListElem(data, 0, tail_);
tail_ = tail_->next;
}
++size_;
}
1) I hear often that anything can fail in a C++ program. Is it realistic to test if the constructor for ListElem fails (or tail_ during new
ing)?
2) Would it ever be necessary to test the type of data (currently a simple typedef int T
until I templatize everything) to make sure the type is viable for the structure?
I realize that these are overly simple examples, but I'm currently just confused as to when I should actually practice good ES and when it's not.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
绝对抛出异常。
用户必须知道如果列表为空会发生什么 - 否则调试起来会很困难。用户不会被迫使用try/catch 语句;如果异常是意外的(即只能由于程序员错误而发生),则没有理由尝试捕获它。当异常未被捕获时,它会被 std::terminate 处理并且这是非常有用的行为。无论如何,try/catch 语句本身也不慢;成本是实际抛出异常和展开堆栈。如果不抛出异常,则几乎不需要任何成本。
尽可能具体。使用您自己的错误类是执行此操作的最佳方法。使用继承对相关异常进行分组(以便调用者可以更轻松地捕获它们)。
如果
head_
为 null,则取消引用它(作为尝试分配给head_->prev
的一部分)是未定义行为。抛出异常是未定义行为的可能后果,但不太可能发生(它要求编译器不遗余力地握住你的手,在一种这种事情被认为是荒谬的语言中;)),而不是一个我们担心的是,因为未定义的行为就是未定义的行为 - 这意味着你的程序无论如何都已经是错误的,并且尝试使错误的方式变得更正确是没有意义的。另外,您已经显式检查
head_
是否不为 null。因此,假设您没有对线程执行任何操作,那么就没有问题。这有点偏执。 :)
如果
new
失败,则会抛出std::bad_alloc
实例。抛出异常正是您想要在这里发生的事情,因此您不想或不需要做任何事情 - 只需让它传播即可。将错误重新描述为某种列表异常并不能真正添加有用的信息,并且可能只会进一步掩盖事情。如果构造函数 ListElem 失败,它应该通过抛出异常而失败,并且大约是 999 比 1,您也应该让该异常失败。
这里的关键是,每当这里抛出异常时,不需要做任何清理工作,因为您还没有修改列表,并且构造/新建的对象正式从未存在(TM)。只要确保其构造函数是异常安全的,就可以了。如果
new
调用无法分配内存,则构造函数甚至不会被调用。当您在同一位置进行多次分配时,您必须担心。在这种情况下,您必须确保如果第二次分配失败,您将捕获异常(无论是什么),清理第一次分配,然后重新抛出。否则,您会泄漏第一个分配。
类型在编译时进行检查。实际上,您无法在运行时对它们执行任何操作,也实际上不需要这样做。 (如果您不需要所有类型检查,那么为什么要使用一种强制您在各处专门键入类型名的语言?:))
Absolutely throw the exception.
The user must know what happened if the list was empty - otherwise it will be hell to debug. The user is not forced to use try/catch statements; if the exception is unexpected (i.e. can only occur due to programmer error), then there is no reason to try to catch it. When an exception goes uncaught, it falls through to std::terminate and this is very useful behaviour. The try/catch statements themselves aren't slow, either, anyway; what costs is the actual throwing of the exception and unwinding of the stack. It costs approximately nothing if the exception doesn't get thrown.
Be as specific as you can. Using your own error classes is the best way to do this. Use inheritance to group related exceptions (so that callers can catch them more easily).
If
head_
is null, then dereferencing it (as part of the attempt to assign tohead_->prev
) is undefined behaviour. Throwing an exception is a possible consequence of undefined behaviour, but an unlikely one (it requires the compiler to be going out of its way to hold your hand, in a language where that sort of thing is considered absurd ;) ), and not one that we worry about, because undefined behaviour is undefined behaviour - it means your program is already wrong anyway, and there's no point in trying to make the way in which it's wrong be more right.Plus, you're already explicitly checking that
head_
isn't null anyway. So there's no problem, assuming you aren't doing anything with threading.That's slightly paranoid. :)
If the
new
fails, then an instance ofstd::bad_alloc
is thrown. Throwing an exception is exactly what you want to happen here, so you don't want or need to do anything - just let it propagate. Re-describing the error as some kind of list exception is not really adding useful information and may just obscure things further.If the constructor ListElem fails, it should fail by throwing an exception, and it's about 999 to 1 that you should just let that one fall through, too.
The key here is that whenever an exception gets thrown here, there is no cleanup work to do, because you haven't modified the list yet, and the constructed/newed object Officially Never Existed(TM). Just make sure that its constructor is exception-safe, and you'll be fine. If the
new
call fails to allocate memory, the constructor doesn't even get called.The time when you have to worry is when you are making more than one allocation in the same place. In this case, you have to make sure that if the second allocation fails, you catch the exception (no matter what it is), clean up the first allocation, and re-throw. Otherwise, you leak the first allocation.
Types are checked at compile time. You can't realistically do anything about them at runtime, nor would you ever realistically need to. (If you don't want all that type-checking, then why are you using a language that forces you to type in the typenames exclusively all over the place? :) )
你应该始终努力争取异常安全。请注意,“异常安全”并不意味着“如果出现问题则抛出异常”。它的意思是“提供三种异常保证之一:弱、强或不抛出”。抛出异常是可选的。为了让代码的调用者确信他们的代码在发生错误时可以正确运行,异常安全是必要的。
您会看到不同 C++ 程序员/团队对于异常的风格截然不同。有些人经常使用它们,另一些人则几乎不使用它们(或者甚至根本不使用它们,尽管我认为现在这种情况相当罕见。Google 可能是最著名的例子,如果您感兴趣,请检查他们的 C++ 风格指南以了解其原因)嵌入式设备和游戏的内部结构可能是下一个最有可能找到人们在 C++ 中完全避免异常的示例的地方)。标准 iostreams 库允许您在流上设置一个标志,以便在发生 I/O 错误时是否应抛出异常。默认值是不,这对于来自几乎任何其他存在异常的语言的程序员来说都是一个惊喜。
这不是“列表”失败,而是当列表为空时调用
pop_front
失败。您不能概括类上的所有操作,它们应该始终在失败时抛出异常,您必须考虑特定情况。在这种情况下,您至少有五个合理的选择:pop_front
是未定义的行为,然后忽略pop_front
代码中的可能性。弹出一个空的标准容器是 UB,并且某些标准库实现不包含检查代码,尤其是在发布版本中。assert
的用途),在这种情况下,您还可以选择触发调试器断点。除最后一个之外的所有这些都意味着您的函数可以提供“不抛出”保证。您选择哪一个取决于您希望 API 是什么样子,以及您希望为调用者提供什么样的帮助来查找错误。请注意,抛出异常不会强制您的直接调用者捕获它。异常只能由能够从错误中恢复的代码捕获(或者可以选择在程序的最顶部)。
就我个人而言,我倾向于不为用户错误抛出异常,并且我也倾向于说弹出空列表是用户错误。这并不意味着在调试模式下进行各种检查没有用,只是我通常不会定义 API 来保证在所有模式下执行此类检查。
?不,这不是必需的,因为这是一个可以避免的错误。调用者始终可以通过在调用
pop_front
之前检查列表是否为空来确保它不会被抛出。std::logic_error
将是一个完全合理的异常抛出。使用特殊异常类的主要原因是调用者可以捕获该异常:由您决定调用者是否需要针对特定情况执行此操作。除非你的程序以某种方式引发了未定义的行为。所以是的,您可以在此之前减少大小,并且可以在删除之前减少它,前提是您确定 ListElem 的析构函数不会抛出异常。当编写任何析构函数时,您应该确保它不会抛出异常。
并非一切都可能失败。理想情况下,函数应该记录它们提供的异常保证,这反过来又告诉您它们是否可以抛出。如果他们确实有详细的记录,他们会列出他们可以抛出的所有东西,以及他们在什么情况下抛出它。
您不应该测试new 是否失败,您应该允许
new
的异常(如果有)从您的函数传播到调用者。然后,您可以记录push_front
可以抛出std::bad_alloc
来指示内存不足,也许它还可以抛出的复制构造函数抛出的任何内容>T
(对于int
来说什么也没有)。您可能不需要为每个函数单独记录这一点 - 有时涵盖多个函数的一般注释就足够了。如果一个名为push_front
的函数可以抛出异常,那么它可能抛出的异常之一就是bad_alloc
,这对任何人来说都不应该感到惊讶。对于模板容器的用户来说,如果所包含的元素抛出异常,那么这些异常可以被传播,这也应该不足为奇。您可以编写您的结构,使得 T 所需要的只是它是可复制构造和可分配的。无需为此添加特殊测试 - 如果有人尝试使用不支持您对其执行的操作的类型来实例化您的模板,他们将收到编译错误。不过,您应该记录这些要求。
You should always strive for exception safety. Note that "exception safety" doesn't mean, "throwing an exception if anything goes wrong". It means "providing one of the three exception guarantees: weak, strong or nothrow". Throwing exceptions is optional. Exception safety is necessary to allow callers of your code to be satisfied that their code can operate correctly when errors occur.
You will see very different styles from different C++ programmers/teams regarding exceptions. Some use them a lot, others hardly at all (or even strictly not at all, although I think that's fairly rare now. Google is probably the most (in)famous example, check their C++ style guide for their reasons if you're interested. Embedded devices and the innards of games are probably the next most likely places to find examples of people avoiding exceptions entirely in C++). The standard iostreams library lets you set a flag on streams whether they should throw exceptions when I/O errors occur. The default is not to, which comes as a surprise to programmers from almost any other language in which exceptions exist.
It's not "a list" failing, it's specifically
pop_front
being called when the list is empty that fails. You can't generalize over all operations on a class, that they should always throw exceptions on failure, you have to consider specific cases. In this case you have at least five reasonable options:pop_front
when the list is empty, then ignore the possibility in the code forpop_front
. It's UB to pop an empty standard container, and some standard library implementations contain no checking code, especially in release builds.assert
is for), in which case you might also have the option of triggering a debugger breakpoint.All of these except the last mean that your function can offer the "nothrow" guarantee. Which one you choose depends what you want your API to look like, and what kind of help you want to give your callers in finding their bugs. Note that throwing an exception does not force your immediate caller to catch it. Exceptions should only be caught by code that's capable of recovering from the error (or optionally at the very top of the program).
Personally, I lean toward not throwing exceptions for user errors, and I also lean toward saying that popping an empty list is a user error. This doesn't mean that in debug mode it isn't useful to have all kinds of checks, just that I don't usually define APIs to guarantee such checks will be performed in all modes.
No, it's not necessary, because this is an avoidable error. A caller can always ensure that it won't be thrown, by checking that the list is non-empty before calling
pop_front
.std::logic_error
would be a perfectly reasonable exception to throw. The main reason to use a special exception class is so that callers can catch just that exception: it's up to you whether you think callers will need to do that for a particular case.Not unless your program has somehow provoked undefined behavior. So yes, you can decrement the size before that, and you can decrement it before the
delete
provided you're sure the destructor of ListElem can't throw. And when writing any destructor, you should ensure that it doesn't throw.It's not true that everything can fail. Ideally functions should document what exception guarantee they offer, which in turn tells you whether they can throw or not. If they're really well-documented, they'll list everything they can throw, and under what circumstances they throw it.
You shouldn't test whether
new
fails, you should allow the exception fromnew
, if any, to propagate from your function to your caller. Then you can just document thatpush_front
can throwstd::bad_alloc
to indicate lack of memory, and perhaps also that it can throw anything that's thrown by the copy constructor ofT
(nothing, in the case ofint
). You might not need to document this separately for each function - sometimes a general note covering multiple functions is sufficient. It shouldn't come as a huge surprise to anyone that if a function calledpush_front
can throw, then one of the things it can throw isbad_alloc
. It should also come as no surprise to users of a template container than if the contained elements throw exceptions, then those exceptions can be propagated.You can probably write your structure such that all is required of T is that it's copy-constructable and assignable. There's no need to add special tests for this - if someone tries to instantiate your template with a type that doesn't support the operations you perform on it, they'll get a compilation error. You should document the requirements, though.
这是一个很长的问题。我将回答所有编号为
1)
的问题。不会。如果您的用户关心性能,他们会在尝试弹出之前检查长度,而不是弹出并捕获异常。例外情况是,如果用户忘记先检查长度,则通知他们,此时您确实希望应用程序在他们面前爆炸。如果您什么都不做,可能会导致稍后才会出现的微妙问题,这将使调试变得更加困难。
例如,如果内存不足,构造函数可能会失败,但在这种情况下,它应该抛出异常,而不是返回 null。因此,您不需要显式测试构造函数是否失败。有关更多详细信息,请参阅此问题:
That's a long question. I'll take all the questions that are numbered
1)
.No. If your user cares about performance they will check the length before attempting to pop rather than popping and catching the exception. The exception is there to inform your user if they forget to check the length first, and at that point you really want the application to blow up in their face. If you just do nothing it could cause subtle problems which only show up later, and that will make debugging more difficult.
The constructor can for example fail if you run out of memory, but in this case it should throw an exception, not return null. So you shouldn't need to test explicitly for the constructor failing. See this question for more details:
是的,这是现实的。否则,如果您的程序内存不足并且分配失败(或者构造函数由于其他内部原因而失败),那么稍后您将会遇到问题。
基本上,当代码无法完全执行其 API 声明它将执行的操作时,您必须随时发出失败信号。
唯一的区别在于如何发出失败信号 - 通过返回值或异常。如果存在性能考虑,返回值可能比异常更好。但这两种方法都需要调用者中特殊的错误捕获代码。
Yes, it is realistic. Otherwise, if your program runs out of memory and allocation fails (or the constructor fails for some other internal reason), you will have problems later on.
Basically, you must signal a failure ANY time when the code is unable to fully do something its API declares it will do.
The only difference is how you signal the failure - via return value or via exception. If performance considerations exist, return values may be better than exceptions. But both approaches require special error catching code in the caller.
对于你的第一组问题:
else
中的所有语句都提供不抛出保证。对于您的第二组:
不,测试不现实。您不知道底层构造函数可能会抛出什么。它可能是一个预期的项目(即
std::bad_alloc
),也可能是一些奇怪的东西(即int
),因此处理它的唯一方法是将其放入catch(...)
中,这是邪恶的:)另一方面,您现有的方法已经是异常安全的,只要在
if
块内创建的虚拟结束节点将被链表的析构函数删除。 (即new
之后的所有内容均不提供抛出)假设对
T
的任何操作都可以抛出异常,析构函数除外。For your first set of questions:
else
provide the nothrow guarantee.For your second set:
No, it's not realistic to test. You don't have any idea what the underlying constructor could throw. It could be an expected item (i.e.
std::bad_alloc
) or it could be something strange (i.e.int
), and therefore the only way you could handle it would be to put it inside acatch(...)
which is evil :)On the other hand, your existing method is already exception safe, so long as the dummy end node created inside the
if
block will be nuked by the destructor of your linked list. (i.e. everything after thenew
s provides nothrow)Just assume any operation on
T
can throw, except the destructor.