Home > Software engineering >  Throwing data member's destructor
Throwing data member's destructor

Time:09-27

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?

CodePudding user response:

Destructors are by default noexpect(true) unless specified explicitly otherwise, or unless a 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 states 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 will happen. I.e no other destructurs will be called, that might cause more harm than the undead object.

If you really want to be safe, 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;
};

CodePudding user response:

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

CodePudding user response:

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.

  •  Tags:  
  • c
  • Related