C++核心准则​讨论:析构,释放和交换操作必须永不失败

共 7323字,需浏览 15分钟

 ·

2020-12-11 00:27

Discussion: Destructors, deallocation, and swap must never fail

讨论:析构,释放和交换操作必须永不失败


Never allow an error to be reported from a destructor, a resource deallocation function (e.g., operator delete), or a swap function using throw. It is nearly impossible to write useful code if these operations can fail, and even if something does go wrong it nearly never makes any sense to retry. Specifically, types whose destructors might throw an exception are flatly forbidden from use with the C++ Standard Library. Most destructors are now implicitly noexcept by default.

永远不要允许从析构函数,资源释放函数(例如,运算符删除)或交换函数中使用throw报告错误。如果这些操作失败,编写有用的代码几乎是不可能的,发生错误,重试也几乎没有任何意义。特别是,析构函数可能引发异常的类型已经被明确禁止与C ++标准库一起使用。现在默认情况下,大多数析构函数都隐式地为noexcept。


Example(示例)

class Nefarious {
public:
Nefarious() { /* code that could throw */ } // ok
~Nefarious() { /* code that could throw */ } // BAD, should not throw
// ...
};
  1. Nefarious objects are hard to use safely even as local variables:

    邪恶对象很难安全地用作局部变量:

     void test(string& s)
    {
    Nefarious n; // trouble brewing
    string copy = s; // copy the string
    } // destroy copy and then n

    Here, copying s could throw, and if that throws and if n's destructor then also throws, the program will exit via std::terminate because two exceptions can't be propagated simultaneously.

    在这里,复制s可能会抛出异常,而且抛出异常时,如果n的析构函数也抛出异常,则程序将通过std :: terminate退出,因为两个异常不能同时传播。

  2. Classes with Nefarious members or bases are also hard to use safely, because their destructors must invoke Nefarious' destructor, and are similarly poisoned by its bad behavior:

    具有Nefarious成员或基类的类也很难安全地使用,因为它们的析构函数必须调用Nefarious的析构函数,并且类似地会因其不良行为而中毒:

     class Innocent_bystander {
    Nefarious member; // oops, poisons the enclosing class's destructor
    // ...
    };

    void test(string& s)
    {
    Innocent_bystander i; // more trouble brewing
    string copy2 = s; // copy the string
    } // destroy copy and then i

    Here, if constructing copy2 throws, we have the same problem because i's destructor now also can throw, and if so we'll invoke std::terminate.

    在这里,如果copy2的构造过程抛出异常,我们将遇到相同的问题,因为我的析构函数现在也可能抛出异常,如果是,std :: terminate将会被触发。

  3. You can't reliably create global or static Nefarious objects either:

    您不能可靠地创建全局或静态Nefarious对象:

     static Nefarious n;       // oops, any destructor exception can't be caught
  4. You can't reliably create arrays of Nefarious:

    您无法可靠地创建Nefarious数组:

     void test()
    {
    std::array arr; // this line can std::terminate(!)
    }

    The behavior of arrays is undefined in the presence of destructors that throw because there is no reasonable rollback behavior that could ever be devised. Just think: What code can the compiler generate for constructing an arr where, if the fourth object's constructor throws, the code has to give up and in its cleanup mode tries to call the destructors of the already-constructed objects ... and one or more of those destructors throws? There is no satisfactory answer.

    如果存在引发异常的析构函数,数组的行为是不确定的,因为没有合理的回滚行为可以设计。试想一下:编译器可以生成什么代码来构造arr,如果第四个对象的构造函数抛出该代码,则该代码必须放弃,并在其清理模式下尝试调用已构造对象的析构函数...这些更多的析构函数会抛出异常么?没有令人满意的答案。

  5. You can't use Nefarious objects in standard containers:

    您不能在标准容器中使用Nefarious对象:

     std::vector vec(10);   // this line can std::terminate()

    The standard library forbids all destructors used with it from throwing. You can't store Nefarious objects in standard containers or use them with any other part of the standard library.

    标准库禁止所有与其一起使用的析构函数抛出异常。您不能将Nefarious对象存储在标准容器中,也不能将它们与标准库的任何其他部分一起使用。

Note(注意)

These are key functions that must not fail because they are necessary for the two key operations in transactional programming: to back out work if problems are encountered during processing, and to commit work if no problems occur. If there's no way to safely back out using no-fail operations, then no-fail rollback is impossible to implement. If there's no way to safely commit state changes using a no-fail operation (notably, but not limited to, swap), then no-fail commit is impossible to implement.

这些是必不可少的关键功能,因为它们是事务编程中两个关键操作所必需的:如果在处理过程中遇到问题,则回滚工作;如果没有问题,则提交工作。如果无法使用无失败操作安全地退出,则无失败回滚是不可能实现的。如果无法使用无失败操作(特别是但不限于交换)来安全地提交状态更改,那么就不可能实现无失败提交。

Consider the following advice and requirements found in the C++ Standard:

考虑C ++标准中的以下建议和要求:

If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. --[C++03] §15.2(3)

如果在堆栈展开期间调用的析构函数异常退出,则将终止(15.5.1)。因此,析构函数通常应捕获异常,而不应让它们传播出析构函数。-[C ++ 03]§15.2(3)

No destructor operation defined in the C++ Standard Library (including the destructor of any type that is used to instantiate a standard-library template) will throw an exception. --[C++03] §17.4.4.8(3)

C ++标准库中定义的析构函数操作(包括用于实例化标准库模板的任何类型的析构函数)都不会引发异常。-[C ++ 03]§17.4.4.8(3)

Deallocation functions, including specifically overloaded operator delete and operator delete[], fall into the same category, because they too are used during cleanup in general, and during exception handling in particular, to back out of partial work that needs to be undone. Besides destructors and deallocation functions, common error-safety techniques rely also on swap operations never failing -- in this case, not because they are used to implement a guaranteed rollback, but because they are used to implement a guaranteed commit. For example, here is an idiomatic implementation of operator= for a type T that performs copy construction followed by a call to a no-fail swap:

取消分配功能(特别包括重载的运算符delete和operator delete [])属于同一类,因为它们通常在清理过程中(尤其是在异常处理过程中)也用于撤消需要撤消的部分工作。除了析构函数和释放函数之外,常见的安全的错误处理技术还依赖于永不失败的交换操作-在这种情况下,不是因为它们用于实现有保证的回滚,而是因为它们用于实现有保证的提交。例如,以下是对类型T的operator =的惯用实现,该类型T执行拷贝构造,然后调用无失败交换:

T& T::operator=(const T& other)
{
auto temp = other;
swap(temp);
return *this;
}

(See also Item 56. ???)

(另请参阅第56项。)

Fortunately, when releasing a resource, the scope for failure is definitely smaller. If using exceptions as the error reporting mechanism, make sure such functions handle all exceptions and other errors that their internal processing might generate. (For exceptions, simply wrap everything sensitive that your destructor does in a try/catch(...) block.) This is particularly important because a destructor might be called in a crisis situation, such as failure to allocate a system resource (e.g., memory, files, locks, ports, windows, or other system objects).

幸运的是,释放资源时,失败的范围肯定较小。如果使用异常作为错误报告机制,请确保此类函数处理其内部处理可能生成的所有异常和其他错误。(对于例外情况,只需将您的析构函数所做的所有敏感操作都包装在try / catch(...)块中。)这尤其重要,因为在危机情况下可能会调用析构函数,例如无法分配系统资源(例如,,内存,文件,锁,端口,窗口或其他系统对象)。

When using exceptions as your error handling mechanism, always document this behavior by declaring these functions noexcept. (See Item 75.)

当使用异常作为错误处理机制时,请始终通过声明这些函数noexcept来说明此类行为。(请参阅第75条。)


原文链接https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#discussion-destructors-deallocation-and-swap-must-never-fail

新书介绍

《实战Python设计模式》是作者最近出版的新书,拜托多多关注!

本书利用Python 的标准GUI 工具包tkinter,通过可执行的示例对23 个设计模式逐个进行说明。这样一方面可以使读者了解真实的软件开发工作中每个设计模式的运用场景和想要解决的问题;另一方面通过对这些问题的解决过程进行说明,让读者明白在编写代码时如何判断使用设计模式的利弊,并合理运用设计模式。

对设计模式感兴趣而且希望随学随用的读者通过本书可以快速跨越从理解到运用的门槛;希望学习Python GUI 编程的读者可以将本书中的示例作为设计和开发的参考;使用Python 语言进行图像分析、数据处理工作的读者可以直接以本书中的示例为基础,迅速构建自己的系统架构。




觉得本文有帮助?请分享给更多人。

关注微信公众号【面向对象思考】轻松学习每一天!

面向对象开发,面向对象思考!



浏览 53
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报