9

I have a class that holds an object which destructor's can throw (it is actually a tbb::task_group, but I named it MyObject for simplicity here).

The code is like this:

#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;
};

And the compiler raises the following error:

exception specification of overriding function is more lax than base version

I do not like the idea of spreading noexcept(false) all over the code so I was thinking about using raw pointers to MyObject, and deleting them outside of the destructor (e.g. in a Close function).

What is the best way to handle that kind of scenario?

Uraza
  • 556
  • 3
  • 17
  • I found out that using std::unique_ptr in class B silences the error. Not sure that it is a good idea though, based on answers to this question: https://stackoverflow.com/questions/37788282/why-exceptions-from-unique-ptr-destructor-terminates-the-program – Uraza Sep 27 '21 at 08:19
  • Throwing exceptions from destructors isn't a good idea to begin with. Note that adding noexcept will have the effect of terminating your program if an exception does happen. What you can do is have try/catch blocks in your destructors to handle things that do go wrong. But in general, don't throw in destructors – Pepijn Kramer Sep 27 '21 at 08:26
  • Destructors which are potentially throwing are a bad idea in general: [C++ FAQ: How can I handle a destructor that fails?](https://www.cs.technion.ac.il/users/yechiel/c++-faq/dtors-shouldnt-throw.html) and [Andrzej's C++ blog: Destructors that throw](https://akrzemi1.wordpress.com/2011/09/21/destructors-that-throw/). You still have the option to put everything in a `try catch` block or leave the destructor noexcept at least (which may call `std::terminate()` if an exception is called for all that). Concerning the destructors of members and base classes... They should be noexcept as well. ;-) – Scheff's Cat Sep 27 '21 at 08:26
  • 1
    The throwing destructor is actually coming from an external library (tbb::task_group more specifically, as I mentioned at the beginning). – Uraza Sep 27 '21 at 08:29
  • 1
    @Uraza Destructor of `unique_ptr` requires that the application of deleter does not throw: https://en.cppreference.com/w/cpp/memory/unique_ptr/~unique_ptr. You would need to use a custom deleter with exception ignoration. Or, use a raw pointer. Or, use just an _aligned storage_ with manual lifetime management (if you want to avoid the overdead of dynamic allocations, which is likely not the case here). – Daniel Langr Sep 27 '21 at 08:36
  • You should not do anything outside the destructor. Just declare A's destructor `noexcept(false)`. B`s destructor should be the noexcept(false), too. – Bernd Sep 27 '21 at 08:45
  • @Bernd You likely didn't read the question carefully enough. Note that: _"I do not like the idea of spreading `noexcept(false)` all over the code..."_ – Daniel Langr Sep 27 '21 at 08:51
  • No, in B you don't need to repeat the exception specifier. It is implicitly noexcept false if A is noexcept... – Bernd Sep 27 '21 at 09:02

3 Answers3

5

Destructors are by default noexpect(true) unless specified explicitly otherwise, or unless a base's or member's destructor can throw. The latter is your case. After that it is a simple mismatch between function signatures.

virtual ~A() {} is thus actually virtual ~A() noexcept {} which does not match virtual ~B() noexcept(false) {}.

You have two solutions:

  1. Explicitly mark ~B as noexcept(true), but if ~MyObject throws, the program is terminated at ~B's boundary.
  2. Mark ~A also noexcept(false),

Throwing from destructors is a really bad idea. Throwing signals that the object cannot be destroyed, is that really what is happening in your code? Throw only if the correct response is to immediately terminate the program, because that is what can happen if the destructor is called as part of stack unwinding. In that case no other destructors will be called, that might cause more harm than the undead object.

If you really want to be safe and do not care about the thrown exception, you can wrap the member in a unique_ptr with an absorbing destructor:

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;
};
Quimby
  • 17,735
  • 4
  • 35
  • 55
  • The throwing destructor is actually coming from an external library (tbb::task_group): it can throw when the caller does not wait for the task to complete before destroying it. – Uraza Sep 27 '21 at 08:33
  • 3
    fwiw, i was sceptical and had to check for myself. `tbb::task_group` indeed throws when `wait` has not been called before https://spec.oneapi.io/versions/0.5.0/oneTBB/task_scheduler/task_groups/task_group_cls.html – 463035818_is_not_an_ai Sep 27 '21 at 08:36
  • @463035818_is_not_a_number I was skeptical too (and surprised as well). How could they...? – Scheff's Cat Sep 27 '21 at 08:37
  • 2
    @Uraza perhaps wrapping the task_group in some custom class with a `noexcept` destructor is an option. Throwing from destructors is really evil – 463035818_is_not_an_ai Sep 27 '21 at 08:37
  • 1
    @Uraza I cannot pretend to know why Intel chose this strategy, but throwing in destructor is really really bad and should be avoided at all costs. That's why [`std::thread::~thread`](https://en.cppreference.com/w/cpp/thread/thread/~thread) calls `std::terminate` instead of throwing – bolov Sep 27 '21 at 08:37
  • @Uraza Sorry, did not know it was an external lib. – Quimby Sep 27 '21 at 08:39
  • 1
    ["Requires: Method wait must be called before destroying a task_group, otherwise the destructor throws an exception."](https://spec.oneapi.io/versions/0.5.0/oneTBB/task_scheduler/task_groups/task_group_cls.html) – 463035818_is_not_an_ai Sep 27 '21 at 08:40
  • 1
    @Quimby Yes, that's a weird design from TBB. :) – Uraza Sep 27 '21 at 08:41
  • 1
    @463035818_is_not_a_number I like the idea of wrapping it: if the wrapper always waits, it is kind of guaranteed that the exception would not happen. – Uraza Sep 27 '21 at 08:43
1

Based on a suggestion from @463035818_is_not_a_number, it is possible to wrap the throwing class into a custom one that does not throw.

In my case, for tbb::task_group, it could be like this:

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;
};
Uraza
  • 556
  • 3
  • 17
0

Generally speaking, if I had to handle this situation, I'd force the destructor of B to be noexcept(true). For example;

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;
};

This will allow the code to compile, but the down-side is that whenever destruction of _object throws an exception (in MyObjects destructor), std::terminate() will be called.

That case can, however, be handled if there are some actions that can be taken to prevent the destruction of _object throwing. These actions can be performed in the destructor of B, making use of the fact that the destructor of B is called before the destructors of any of Bs bases or members. In other words, modify Bs destructor in the above to

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

If there are no actions that can be taken to prevent destruction of _object from throwing, then you'll have to live with the program terminating whenever _objects destruction throws.

My statement above that the destructor of B will be called before the destructor of Object or of class A is a consequence of two rules in the standard. Construction of an object constructs bases and members, and then the constructor of the most derived class is called. And the order of destruction for an object (sequence of destructor calls) is the reverse of the order of construction.

Personally, if I had a third-party library that provided a throwing destructor in a way that can't be prevented, I'd seriously consider either finding a different library, or rolling my own equivalent library that does not have a throwing destructor.

Peter
  • 35,646
  • 4
  • 32
  • 74