【问题标题】:Throwing data member's destructor抛出数据成员的析构函数
【发布时间】:2021-11-19 09:36:25
【问题描述】:

我有一个类,它包含一个析构函数可以抛出的对象(它实际上是一个 tbb::task_group,但为了简单起见,我在这里将其命名为 MyObject)。

代码是这样的:

#include <stdexcept>

class MyObject {
public:
    MyObject() {}
    ~MyObject() noexcept(false) {}
};

class A {
public:
    A() {}
    virtual ~A() {}
};

class B : public A {
public:
    B() : A() {}
    ~B() {}

private:
    MyObject _object;
};

并且编译器会引发以下错误:

覆盖函数的异常规范比基础版本更宽松

我不喜欢在整个代码中传播 noexcept(false) 的想法,所以我正在考虑使用指向 MyObject 的原始指针,并在析构函数之外删除它们(例如在 Close 函数中)。

处理这种情况的最佳方法是什么?

【问题讨论】:

  • 我发现在 B 类中使用 std::unique_ptr 可以消除错误。根据对这个问题的回答,不确定这是一个好主意:stackoverflow.com/questions/37788282/…
  • 从析构函数中抛出异常并不是一个好主意。请注意,如果确实发生异常,添加 noexcept 将具有终止程序的效果。你可以做的是在你的析构函数中使用 try/catch 块来处理出错的事情。但一般来说,不要扔析构函数
  • 可能抛出的析构函数通常是个坏主意:C++ FAQ: How can I handle a destructor that fails?Andrzej's C++ blog: Destructors that throw。您仍然可以选择将所有内容放在 try catch 块中,或者至少保留析构函数 noexcept(如果所有这些都调用了异常,它可能会调用 std::terminate())。关于成员和基类的析构函数......它们也应该是 noexcept 。 ;-)
  • 抛出的析构函数实际上来自外部库(更具体地说,是 tbb::task_group,正如我在开头提到的那样)。
  • unique_ptr的@Uraza析构函数要求deleter的应用不抛出:en.cppreference.com/w/cpp/memory/unique_ptr/~unique_ptr。您需要使用带有异常忽略的自定义删除器。或者,使用原始指针。或者,仅使用具有手动生命周期管理的对齐存储(如果您想避免动态分配的过时,这里可能不是这种情况)。

标签: c++


【解决方案1】:

析构函数默认为noexpect(true),除非另有明确指定,或者除非基类或成员的析构函数可以抛出。后者是你的情况。之后就是函数签名之间的简单不匹配。

virtual ~A() {} 因此实际上是virtual ~A() noexcept {},与virtual ~B() noexcept(false) {} 不匹配。

您有两种解决方案:

  1. ~B显式标记为noexcept(true),但如果~MyObject抛出,程序将在~B的边界处终止。
  2. 标记~A 也标记noexcept(false)

从析构函数中抛出是一个非常糟糕的主意。抛出对象不能被破坏的信号,这真的是你的代码中发生的事情吗?仅当正确的响应是立即终止程序时才抛出,因为如果将析构函数作为堆栈展开的一部分调用,就会发生这种情况。在这种情况下,不会调用其他析构函数,这可能会比不死对象造成更大的伤害。

如果你真的想要安全并且不关心抛出的异常,你可以使用吸收析构函数将成员包装在 unique_ptr 中:

class B : public A {
public:
    B() : A(), _object{new MyObject,deleter} {}
     ~B()  noexcept(true) {}

private:
    constexpr static auto deleter = [](MyObject* obj){ try { delete obj;}catch(...){};};
    std::unique_ptr<MyObject,decltype(deleter)> _object;
};

【讨论】:

【解决方案2】:

基于来自@463035818_is_not_a_number 的a suggestion,可以将 throwing 类包装成一个不抛出的自定义类。

在我的例子中,对于 tbb::task_group,它可能是这样的:

class TaskGroup {
public:
    TaskGroup() {
        _task = new tbb::task_group();
    }

    // This destructor will not throw.
    // Not very pleased with "catch (...)" but not much choice due to TBB's implementation.
    ~TaskGroup() {
        try {
            delete _task;
        } catch (...) {}
    }

    // Wrap any other needed method here.
    
private:
    tbb::task_group* _task;
};

【讨论】:

  • 你不需要使用newtbb::taks_group _task; 作为会员应该没问题。目前您需要注意 3/5 规则
  • @463035818_is_not_a_number 如果我使用普通对象而不是新对象,这如何保护我自己的析构函数不被抛出?
  • 哦,对了,我没想到这一点。请注意尊重 3/5 规则
  • 是的,确实如此。感谢您的反馈!
  • @Uraza:您必须使用函数尝试块,然后子对象析构函数中的异常也会被捕获。见stackoverflow.com/a/28685972/103167
【解决方案3】:

一般来说,如果我必须处理这种情况,我会强制B 的析构函数为noexcept(true)。例如;

class MyObject
{
  public:
      MyObject() {}
      ~MyObject() noexcept(false) {};
};

class A
{
  public:
      A() {}
      virtual ~A() {}     // implicitly noexcept(true)
};

class B : public A
{
  public:
      B() : A(), _object() {}      
     ~B() noexcept(true) {};

  private:
     MyObject _object;
};

这将允许代码编译,但不利的一面是,每当_object 的销毁引发异常(在MyObjects 析构函数中),std::terminate() 将被调用。

但是,如果可以采取一些措施来防止破坏_object throwing,则可以处理这种情况。这些操作可以在B 的析构函数中执行,这是利用B 的析构函数在Bs 的任何基或成员的析构函数之前调用的事实。也就是说,将上面Bs的析构函数修改为

     // if within definition of B
     ~B() noexcept(true)
     {
          // take actions to prevent the destructor of _object throwing
     };

如果无法采取任何措施来防止_object 的破坏被抛出,那么您将不得不忍受程序在_objects 破坏抛出时终止。

我上面的声明是B 的析构函数将在ObjectA 类的析构函数之前调用,这是标准中两条规则的结果。对象的构造构造基和成员,然后调用最派生类的构造函数。并且对象的破坏顺序(析构函数调用的顺序)与构造顺序相反。

就我个人而言,如果我有一个第三方库以无法阻止的方式提供了一个抛出析构函数,我会认真考虑找到一个不同的库,或者滚动我自己的没有抛出析构函数。

【讨论】:

    猜你喜欢
    • 2020-06-13
    • 2019-03-09
    • 2015-08-26
    • 2010-10-02
    • 2016-04-12
    • 2016-01-29
    • 2013-01-01
    • 2015-05-14
    相关资源
    最近更新 更多