例外安全 - 何时,如何,为什么?

IAE*_*IAE 10 c++ exception exception-safe

我只是一个初出茅庐的程序员,至少尝试编程而不是最好的情况.我一直在阅读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_;
    }
}
Run Code Online (Sandbox Code Playgroud)

我有一些困境.

1)当列表失败时,我真的应该抛出错误吗?我不应该简单地做什么并返回而不是强迫列表的用户执行try {] catch(){}语句(这也很慢).

2)有多个错误类(加上我的老师要求我们在类中实现的ListException).自定义错误类对于这样的事情是否真的是必需的,并且是否有关于何时使用特定异常类的一般指导?(例如,范围,长度和边界都相似)

3)我知道我不应该更改程序状态,直到抛出异常的所有代码都完成.这就是我最后减小size_的原因.这个简单的例子真的有必要吗?我知道删除不能扔.分配给0时头_-> prev是否可能抛出?(头是第一个节点)

我的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_;
}
Run Code Online (Sandbox Code Playgroud)

1)我经常听到C++程序中的任何事情都会失败.测试ListElem的构造函数是否失败(或者在执行期间是tail_ new)是否现实?

2)是否有必要测试数据类型(目前很简单,typedef int T直到我将所有内容模板化)以确保类型对于结构是可行的?

我意识到这些都是过于简单的例子,但我现在只是对于什么时候我应该练习优秀的ES以及什么时候不这样做感到困惑.

Kar*_*tel 9

当列表失败时,我真的应该抛出错误吗?我不应该简单地做什么并返回而不是强迫列表的用户执行try {] catch(){}语句(这也很慢).

绝对抛出异常.

如果列表为空,用户必须知道发生了什么 - 否则它将是调试的地狱.用户不必强制使用try/catch语句; 如果异常是意外的(即只能由于程序员错误而发生),那么没有理由试图捕获它.当一个异常未被捕获时,它会落到std :: terminate中,这是非常有用的行为.无论如何,try/catch语句本身也不慢; 实际抛出异常和展开堆栈的成本是多少.如果没有抛出异常,它几乎没有任何成本.

有多个错误类(加上我的老师要求我们在类中实现的ListException).自定义错误类对于这样的事情是否真的是必需的,并且是否有关于何时使用特定异常类的一般指导?(例如,范围,长度和边界都相似)

尽可能具体.使用您自己的错误类是执行此操作的最佳方法.使用继承对相关异常进行分组(以便调用者可以更轻松地捕获它们).

我知道我不应该更改程序状态,直到抛出异常的所有代码完成.这就是我最后减小size_的原因.这个简单的例子真的有必要吗?我知道删除不能扔.分配给0时头_-> prev是否可能抛出?(头是第一个节点)

如果head_为null,则取消引用它(作为分配尝试的一部分head_->prev)是未定义的行为.抛出一个异常可能是未定义行为的结果,但是不太可能(它需要编译器不顾一切地握住你的手,这种语言被认为是荒谬的;)),而不是一个我们担心的是,因为未定义的行为是未定义的行为 - 这意味着你的程序无论如何已经是错误的,并且没有必要试图让错误的方式变得更加正确.

另外,你已经明确地检查过head_它不是null.所以没有问题,假设您没有使用线程做任何事情.

我经常听到C++程序中的任何事情都会失败.

那有点偏执.:)

测试ListElem的构造函数是否失败(或者在newing期间是tail_)是否现实?

如果new失败,则std::bad_alloc抛出一个实例.抛出异常正是你想要在这里发生的事情,所以你不想或不需要做任何事情 - 只是让它传播.将错误重新描述为某种列表异常并不是真正添加有用的信息,可能会进一步模糊.

如果构造函数ListElem失败,它应该通过抛出异常失败,并且大约999比1,你应该让它掉下来.

这里的关键是每当抛出一个异常时,就没有清理工作要做,因为你还没有修改过列表,而且构造/新建的对象正式永远不存在(TM).只要确保它的构造函数是异常安全的,你就可以了.如果new调用无法分配内存,则构造函数甚至不会被调用.

您需要担心的时间是在同一个地方进行多次分配.在这种情况下,您必须确保如果第二次分配失败,您将捕获异常(无论它是什么),清理第一次分配,然后重新抛出.否则,您泄漏第一个分配.

是否有必要测试数据类型(目前是一个简单的typedef int T,直到我将所有内容模板化)以确保该类型对于结构是否可行?

在编译时检查类型.你不能在运行时现实地对它们做任何事情,也不会真实地需要.(如果您不想进行所有类型检查,那么为什么使用的语言会强制您在整个地方专门输入类型名?:))


Ste*_*sop 7

我不确定我应该在什么时候努力争取异常安全与速度

你应该始终争取异常安全.请注意,"异常安全"并不意味着"如果出现任何问题就抛出异常".它意味着"提供三种例外保证之一:弱,强或无".抛出异常是可选的.异常安全是必要的,以允许代码的调用者满意他们的代码在发生错误时可以正常运行.

您将看到来自不同C++程序员/团队的关于异常的非常不同的样式.有些人经常使用它们,有些人几乎没用(甚至根本没有用它们,虽然我认为现在这种情况相当罕见.谷歌可能是最着名的例子,如果你感兴趣,可以查看他们的C++风格指南嵌入式设备和游戏内部可能是下一个最有可能在C++中找到完全避免异常的例子的地方.标准的iostreams库允许您在流上设置一个标志,它是否应该在发生I/O错误时抛出异常.默认情况下不是这样,对于几乎任何其他存在例外的语言的程序员来说都是一个惊喜.

当列表失败时,我真的应该抛出错误吗?

它不是"列表"失败,pop_front当列表为空时失败,它就被特别调用.你不能概括一个类的所有操作,他们应该总是在失败时抛出异常,你必须考虑具体的情况.在这种情况下,您至少有五个合理的选择:

  • 返回一个值以指示是否弹出了任何内容.来电者可以做任何他们喜欢的事,或者忽略它.
  • 记录pop_front当列表为空时调用未定义的行为,然后忽略代码中的可能性pop_front.弹出一个空的标准容器是UB,而一些标准库实现不包含检查代码,特别是在发布版本中.
  • 记录它是未定义的行为,但无论如何都要进行检查,要么中止程序,要么抛出异常.您也许只能在调试版本中进行检查(这是assert为了什么),在这种情况下,您可能还可以选择触发调试器断点.
  • 记录如果列表为空,则调用无效.
  • 记录如果列表为空则抛出异常.

所有这些除了最后一个意味着你的功能可以提供"nothrow"保证.您选择哪一个取决于您希望您的API看起来像什么,以及您希望让您的呼叫者找到他们的错误有哪些帮助.请注意,抛出异常不会强制您的直接调用者捕获它.只能通过能够从错误中恢复的代码(或者可选地在程序的最顶层)捕获异常.

就个人而言,我倾向于为用户错误抛出异常,我也倾向于说弹出一个空列表是用户错误.这并不意味着在调试模式下进行各种检查是没有用的,只是因为我通常不定义API来保证在所有模式下执行这样的检查.

这样的事情真的是一个自定义错误类

不,这没有必要,因为这是一个可以避免的错误.通过在调用之前检查列表是否为空,调用者始终可以确保不会抛出它pop_front.std::logic_error投掷将是一个完全合理的例外.使用特殊异常类的主要原因是调用者只能捕获该异常:您是否认为调用者需要针对特定​​情况执行此操作取决于您.

分配给0时头_-> prev是否可能抛出?

除非您的程序以某种方式激起未定义的行为.所以是的,你可以在此之前减小大小,并且你可以在delete提供你确定ListElem的析构函数不能抛出之前递减它.在编写任何析构函数时,你应该确保它不会抛出.

我经常听到C++程序中的任何事情都会失败.测试ListElem的构造函数是否失败(或者在newing期间是tail_)是否现实?

一切都失败并不是真的.理想情况下,函数应记录它们提供的异常保证,这反过来告诉您它们是否可以抛出.如果他们真的记录很好,他们会列出他们可以抛出的一切,以及在什么情况下他们抛出它.

您不应该测试是否new失败,您应该允许异常new(如果有的话)从您的函数传播到调用者.然后你可以只记录push_front可以抛出std::bad_alloc以指示缺少内存的文档,也许它可以抛出由复制构造函数抛出的任何东西T(在这种情况下都没有int).您可能不需要为每个功能单独记录 - 有时候,涵盖多个功能的一般说明就足够了.对于任何人来说,如果一个被调用的函数push_front可以抛出,那么它应该抛出的东西之一就不应该是一个巨大的惊喜bad_alloc.对于模板容器的用户来说,如果包含的元素抛出异常,那么这些异常也可以传播.

是否有必要测试数据类型(目前是一个简单的typedef int T,直到我将所有内容模板化)以确保该类型对于结构是否可行?

您可以编写您的结构,使得T的所有要求都是可复制构造和可分配的.没有必要为此添加特殊测试 - 如果有人试图使用不支持您对其执行的操作的类型来实例化模板,则会出现编译错误.但是,您应该记录要求.


Mar*_*ers 4

这是一个很长的问题。我将回答所有已编号的问题1)

1)当列表失败时我真的应该抛出错误吗?难道我不应该简单地什么也不做并返回,而不是强迫列表的用户执行 try {] catch() {} 语句(这也很慢)。

不会。如果您的用户关心性能,他们会在尝试弹出之前检查长度,而不是弹出并捕获异常。例外情况是,如果用户忘记先检查长度,则通知他们,此时您确实希望应用程序在他们面前爆炸。如果您什么都不做,可能会导致稍后才会出现的微妙问题,这将使调试变得更加困难。

1) 我经常听说 C++ 程序中任何事情都可能失败。测试 ListElem 的构造函数是否失败(或在新建过程中 tail_ 失败)是否现实?

例如,如果内存不足,构造函数可能会失败,但在这种情况下,它应该抛出异常,而不是返回 null。因此,您不需要显式测试构造函数是否失败。请参阅此问题了解更多详细信息: