c++ - 异常安全——何时、如何、为什么?

标签 c++ exception exception-safe

我只是一个初出茅庐的程序员,至少尝试编写超出最佳情况的程序。到目前为止,我一直在阅读 Herb Sutter 的“Exceptional C++”并阅读了三次异常安全章节。但是,除了他提出的示例(Stack)之外,我真的不确定我应该在什么时候争取异常安全与速度以及什么时候这样做很愚蠢。

比如我现在的作业项目是一个双向链表。由于我已经对其中的几个进行了编程,所以我想花时间深入了解一些更深层次的概念,例如 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_ 的原因。在这个简单的例子中,这真的有必要吗?我知道删除不能抛出。 head_->prev 在分配给 0 时是否有可能抛出异常? (head是第一个节点)

我的 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 的构造函数是否失败(或 newing 期间的 tail_)是否现实?

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

我意识到这些都是过于简单的例子,但我目前只是对什么时候应该实践良好的 ES 以及什么时候不应该感到困惑。

最佳答案

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).

绝对抛出异常。

用户必须知道如果列表为空会发生什么 - 否则将很难调试。用户没有被迫使用 try/catch 语句;如果异常是意外的(即只能由于程序员错误而发生),则没有理由 try catch 它。当异常未被捕获时,它会落入 std::terminate 并且这是非常有用的行为。无论如何,try/catch 语句本身也不慢;成本是实际抛出异常和展开堆栈。如果不抛出异常,它几乎不需要任何费用。

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)

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

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)

如果 head_ 为 null,则取消引用它(作为尝试分配给 head_->prev 的一部分)是未定义的行为。抛出异常是未定义行为的可能结果,但不太可能(它要求编译器竭尽全力握住你的手,在一种语言中,这种事情被认为是荒谬的;)),而不是一个我们担心的是,因为未定义的行为就是未定义的行为 - 这意味着您的程序无论如何都已经错了,并且没有必要试图使错误的方式变得更正确。

此外,您已经明确检查 head_ 是否不为空。所以没有问题,假设您没有对线程做任何事情。

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 newing)?

如果 new 失败,则抛出一个 std::bad_alloc 的实例。抛出异常正是您想要在此处发生的,因此您不想或不需要做任何事情 - 只需让它传播即可。将错误重新描述为某种列表异常并不能真正添加​​有用的信息,而且可能只会进一步掩盖事情。

如果构造函数 ListElem 失败,它应该通过抛出异常而失败,而且您也应该让它失败的概率大约为 999 比 1。

这里的关键是,每当这里抛出异常时,没有清理工作要做,因为您还没有修改列表,并且构造/新建的对象 Officially Never Existed( TM值)。只要确保它的构造函数 是异常安全的,就可以了。如果 new 调用无法分配内存,则甚至不会调用构造函数。

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

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?

类型在编译时检查。实际上,您无法在运行时对它们做任何事情,实际上您也不需要这样做。 (如果你不想进行所有类型检查,那你为什么要使用一种语言来强制你在所有地方只输入类型名称?:))

关于c++ - 异常安全——何时、如何、为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4482788/

相关文章:

c++ - 有一个 throwing swap 成员实现可以吗?

c++ - C++中包含的C标准库函数是否抛出异常?

c++ - C++将字符串转换为int

python-3.x - 捕获 Flask API 中引发的自定义异常。引发的所有异常都以 500 错误告终

python - 在 Python/Pydev/Eclipse 中检查异常后的变量

c++ - `myvector.push_back(autoPtr.release())`为什么提供强异常安全保障?

c++ - 用于 MP3、AAC、WAV 的跨平台 (C/C++) 音频库

c++ - 字符串比较显示不相等

c++ - 尝试初始化 __m128 类成员变量时出现 EXC_BAD_ACCESS 信号

java - LayoutParams 上的 ClassCastException 从 LinearLayout 到 AbsListView