【问题标题】:Way for C++ destructor to skip work when specific exception being thrown?C++ 析构函数在抛出特定异常时跳过工作的方式?
【发布时间】:2011-03-20 09:10:28
【问题描述】:

我在堆栈上有一个对象,我希望它的析构函数在调用析构函数时跳过一些工作,因为堆栈正在展开,因为通过堆栈上对象的范围抛出特定异常。

现在我可以在堆栈项的范围内添加一个 try catch 块并捕获有问题的异常并通知堆栈对象不要运行要跳过的工作,然后重新抛出异常,如下所示:

RAII_Class pending;

try {
  doSomeWorkThatMayThrowException();
} catch (exceptionToSkipPendingDtor &err) {
  pending.notifySkipResourceRelease();
  throw;
}

但是,我希望有一种更优雅的方式来做到这一点。例如想象:

RAII_Class::~RAII_Class {
  if (detectExceptionToSkipPendingDtorBeingThrown()) {
    return;
  }
  releaseResource();
}

【问题讨论】:

  • 我立即想到的问题是:你是如何让自己陷入析构函数“认为”它拥有资源但不应该释放它的情况?这对我来说似乎很可疑。我想摆脱这个困境的更好方法是追溯你进入它的方式并采取其他路线。
  • 这种情况出现在客户端服务器架构的情况下,其中连接被抛出异常重置并且尝试释放资源需要与另一方交谈,并且由于重新连接导致另一个会干扰的故障随着重新连接。
  • (您需要正确地@address 评论答案才能显示在我们的答案列表中。我只是不小心偶然发现了这个。)我立即想到解决这个问题的一种方法是让那个 RAII 类使用一些函数来释放资源由 RAII 类的析构函数。

标签: c++ exception destructor raii


【解决方案1】:

std::uncaught_exception() 几乎可以做到这一点,但并不完全如此。

Herb Sutter 比我更好地解释了“几乎”:http://www.gotw.ca/gotw/047.htm

在某些极端情况下,std::uncaught_exception() 从析构函数调用时返回 true,但相关对象实际上并未被堆栈展开过程销毁。

没有 RAII 可能会更好,因为它与您的用例不匹配。 RAII 意味着总是清理;例外与否。

你想要的更简单:只有在没有抛出异常时才释放资源,这是一个简单的函数序列。

explicitAllocateResource();
doSomeWorkThatMayThrowException();
explicitReleaseResource(); // skipped if an exception is thrown
                           // by the previous function.

【讨论】:

  • 只需要一种方法来查询 uncaught_exception 以查看它是否属于特定类型。看来我在问题中写它的方式是唯一的方法。
【解决方案2】:

我会反过来做——如果没有抛出异常,明确告诉它做它的工作:

RAII_Class pending;

doSomeWorkThatMayThrowException();

pending.commit(); // do or prepare actual work

【讨论】:

  • 但是对于其他异常,工作仍然需要完成,所以我仍然需要一个 try/catch 块来处理所有其他异常来执行 commit() 调用。
  • @William:是的,但如果你想在当前范围内捕获,无论如何你都需要 try/catch。唯一的补充是commit() 调用,它也比隐藏机制更清晰和可维护(假设这是完全可能的)。
【解决方案3】:

看起来像 bool std::uncaught_exception();如果您想对每个异常都具有此行为,而不仅仅是特殊异常,则可以做到这一点!

【讨论】:

    【解决方案4】:

    你可以不用try-catch:

    RAII_Class pending;
    doSomeWorkThatMayThrowException();  // intentional: don't release if throw
    pending.releaseResource();
    

    或者,您可以使用 RAII 更加努力地尝试:

    struct RAII_Class {
        template<class Op>
        void execute(Op op) {
            op();
            releaseResources();
        }
    
    private:
        void releaseResources() { /* ... */ }
    };
    
    int main(int argc, char* argv[])
    {
        RAII_Class().execute(doSomeWorkThatMayThrowException);
        return 0;
    }
    

    【讨论】:

    • 是的,这行得通,但它的缺点是它让消费者有责任把它做好,而不是他们能够把它放在堆栈上,一切都按要求工作。
    【解决方案5】:

    这似乎绕过了使用 RAII 的主要原因。 RAII 的重点是,如果您的代码中间发生异常,您仍然可以释放资源/被正确销毁。

    如果这不是您想要的语义,请不要使用 RAII。

    所以而不是:

    void myFunction() {
        WrapperClass wc(acquireResource());
    
        // code that may throw
    }
    

    只要做:

    void myFunction() {
        Resource r = acquireResource();
    
        // code that may throw
    
        freeResource(r);
    }
    

    如果中间的代码抛出,资源不会被释放。这就是您想要的,而不是保留 RAII(并保留名称)但不实现 RAII 语义。

    【讨论】:

    • 其他抛出的异常需要确保释放资源。
    【解决方案6】:

    我发现这个网站有一个关于 std::uncaught_exception() 的有趣讨论,以及对我来说似乎更优雅和正确的替代解决方案:

    http://www.gotw.ca/gotw/047.htm

    //  Alternative right solution
    //
    T::Close() {
      // ... code that could throw ...
    }
    
    T::~T() /* throw() */ {
      try {
        Close();
      } catch( ... ) {
      }
    }
    

    通过这种方式,您的析构函数只做一件事,并且您可以防止在异常期间抛出异常(我认为这是您要解决的问题)。

    【讨论】:

      【解决方案7】:

      虽然它充其量只是一个杂物,但如果您拥有您感兴趣的异常类的代码,您可以向该类(布尔)添加一个静态数据成员,该成员将在该类的对象的构造函数,而析构函数中的 false (可能需要是一个您递增/递减的 int)。然后在您的 RAII 类的析构函数中,您可以检查 std::uncaught_exception(),如果为真,则查询异常类中的静态数据成员。如果您返回 true(或 > 0),则您遇到了这些异常之一 - 否则您将忽略它。

      不是很优雅,但它可能会成功(只要您没有多个线程)。

      【讨论】:

      • 是的,虽然这样可行,但到目前为止,我坚持我在问题中写的方式,因为我觉得这不是一个杂碎。
      • @WilliamKF:我不怪你——但我更喜欢 Georg 关于显式 Commit() 的想法!
      猜你喜欢
      • 2015-08-26
      • 2022-01-16
      • 1970-01-01
      • 2013-04-03
      • 2014-12-06
      • 2013-05-08
      • 2012-04-15
      相关资源
      最近更新 更多