抛出的(默认)构造函数中的异常保证应该是什么

What should be the exception guarantee in a (default) constructor that throws?

本文关键字:是什么 异常 构造函数 默认      更新时间:2023-10-16

我知道构造函数可能会抛出异常,当发生不好的事情时,这可能是一件好事。但是当构造函数抛出并假设构造函数中的所有资源都得到正确管理(例如使用 RAII)时,哪一个是确切的异常保证(基本、强)?

详细地说,我正在记录我的代码并编写每个成员函数的异常保证(并尝试编写安全异常代码)。

例如,如果我有这样的类:

struct A
{
    std::string s;
    A()
    {
        std::vector<int> v(5);
        s = "some text";
        /* do a lot of fascinating things */
        if (error)
            throw 1;
    }
};

当构造函数抛出时,调用vs的析构函数,对吧?因此,多亏了std::vectorstd::string的析构函数,A的构造函数不会泄漏任何资源,然后它至少提供了基本的保证。我说的对吗?

我的问题是:我可以说这个构造函数提供了强有力的保证吗?另外,是否值得记录构造函数的保证?

我的猜测是:它确实有很强的保证。由于对象在尝试构造它之前不存在,并且如果构造函数失败,则无论如何都不会创建对象,因此操作(构造对象)不起作用,并且一切都保留在构造函数开始之前。

如果我的猜测是正确的:

  • 当构造函数只提供基本保证而不是强保证时?

你是对的。构造函数修改的唯一对象是 sv ,它们在调用构造函数之前不存在,在异常退出后也不存在。因此没有可观察到的副作用,构造函数提供了强大的异常保证。

构造函数何时可能仅提供基本保证?可能是人为的例子:

class A {
  public:
    A() {
        printf("A is being constructedn");
        throw std::runtime_error("oh no!");
    }
};

基本保证是相当明显的,但不是强有力的保证,因为有副作用。(创建副作用的另一种方法是修改全局变量。如果构造函数接受参数,可能会发生更有趣的事情。另一个可能是人为的例子:

class B {
  public:
    A(std::vector<int>&& v): v(std::move(v)), a() {}
  private:
    std::vector<int> v;
    A a;
};

在这里,A 的构造函数在 B::v 已经初始化后抛出,所以后者被销毁。调用方仍然具有有效的向量,但它现在为空。由于所有对象都处于有效状态,因此仍然满足基本保证,但不是强保证。

强保证要求如果构造函数抛出,则程序的(逻辑)状态不会改变(除了有一个异常需要处理)。这意味着,如果在任何失败情况下,通过引用或指针、全局变量等传递到构造函数中的对象都没有被更改(或者在构造函数离开之前回滚发生的任何更改),则构造函数将满足强异常保证。

示例代码中的构造函数不适用于任何此类对象,因此它确实提供了强有力的保证。

仅提供基本保证的构造函数示例是

struct foo {
  foo(int &x) : some_resource(10) {
    ++x;
    if(x % 2 == 0) {
      throw "something";
    }
  }
  std::vector<int> some_resource;
};

在这种情况下,基本保证得到了满足 - 在所有情况下some_resource都被清理 - 但强保证不是因为如果抛出异常x仍然会发生变化。

至于文档,这是一个意见问题,所以YMMV。当然,我通常的目标是提供合理可能的最强保证,并记录一个功能满足强保证或不抛出保证,如果我确定我可以永远保持这个保证。记录基本保证不是必需的,因为所有功能都应该提供它。不提供它的功能有一个错误。