3

I have a class whose destructor is noexcept(false). I know it only throws under certain circumstances, and I want to use it as a member variable of a class with a noexcept destructor. From https://en.cppreference.com/w/cpp/language/function-try-block I read "Every exception thrown from any statement in the function body, or (for constructors) from any member or base constructor, or (for destructors) from any member or base destructor, transfers control to the handler-sequence the same way an exception thrown in a regular try block would." which makes me think this should be correct:

#include <exception>

class ConditionallyThrowingDtor {
public:
    bool willThrow = true;
    ConditionallyThrowingDtor() = default;
    ~ConditionallyThrowingDtor() noexcept(false) {
        if (willThrow) {
            throw std::exception();
        }
    }
};


class NonThrowingDtor {
public:
    ConditionallyThrowingDtor x;
    ~NonThrowingDtor() noexcept try {
        x.willThrow = false;
    } catch (...) { 
        // Ignore because we know it will never happen.
    }
};


int main() {
    // ConditionallyThrowingDtor y; // Throws on destruction as expected.
    NonThrowingDtor x;
}

https://godbolt.org/z/ez17fx (MSVC)

My understanding of noexcept and the function-try-block on ~NonThrowingDtor() is that noexcept guarantees it won't throw (and that it does this by essentially doing try { ... } catch (...) { std::terminate(); } https://en.cppreference.com/w/cpp/language/noexcept_spec. But the function-try-block with catch (...) and no additional throw should guarantee it never throws. Clang is OK with this, but as the godbolt link shows, MSVC says

<source>(23): warning C4297: 'NonThrowingDtor::~NonThrowingDtor': function assumed not to throw an exception but does
<source>(23): note: destructor or deallocator has a (possibly implicit) non-throwing exception specification
Ben
  • 9,184
  • 1
  • 43
  • 56
  • Why must the `catch` block necessarily rethrow? Are you saying that in as much as `noexcept` means `try { ... } catch (...) { std::terminate(); }`, that `noexcept try { ... } catch (...) {}` means `try { try { ... } catch (...) { std::terminate(); } } catch (...) {}`? – Ben Nov 09 '20 at 16:00
  • I was under the impression that it was not possible to leave a destructor's `catch` block without implicitly throwing, but apparently you can `return` to explicitly no rethrow. – François Andrieux Nov 09 '20 at 16:08
  • 1
    @Jarod42 Destructors are similar to constructors in that they `throw;` when execution reaches the end of the `catch`. The difference is destructors allow you to `return;` to escape that behavior while constructors do not. I thought they were identical but this is not the case. – François Andrieux Nov 09 '20 at 16:29
  • So warning is legit... Strange that using `return;` don't silent the warning though. – Jarod42 Nov 09 '20 at 16:34
  • 1
    @Ben _"Why must the catch block necessarily rethrow? "_ — Because the standard says so: http://eel.is/c++draft/except.handle#14.sentence-1. – Daniel Langr Nov 09 '20 at 17:08
  • Thank you. That is the right answer, it seems. So, then would a `return;` as suggested above from inside the handler give the behavior I was expecting? If we `return;` inside the function-try-block's handler then control does not reach the end of the handler of the function-try-block of a destructor, correct? – Ben Nov 09 '20 at 17:52
  • @Ben What problem are you trying to solve? It seems to me that there is no one. You just get a warning. I guess that warning are false-positive sometimes. At runtime, there is no exception thrown, therefore everything should work fine. BTW, note that GCC emits a warning as well even when no exception is thrown at runtime: https://godbolt.org/z/175vfq. – Daniel Langr Nov 09 '20 at 17:57
  • I'm trying to have my d'tor be legitimately not throw (or `std::terminate`) when it has a member `tbb::task_group`, which has a throwing d'tor similar to the example I described, where I cain call `m_group.wait()` and know it won't actually throw. I opted for manual management of the lifetime: https://godbolt.org/z/MsovKc but it seems like there should be a better way. – Ben Nov 09 '20 at 18:09
  • Maybe I finally understand. You need to treat the object of `tbb::task_group` as if it would be nothrow-destructible since you 100% know that, in your case, it won't throw, right? And, of course, you cannot change its definition provided by the TBB library. That's an interesting question, but I don't see any other solution that wrapping `tbb::task_group` by some other class. What about wrapping by `std::unique_ptr`? https://godbolt.org/z/fTrTr1 – Daniel Langr Nov 09 '20 at 18:30
  • By “legitimately” I mean no throwing, no `terminate`, no `exit`, just guaranteed clean return. I think my workaround works well since it saves allocation/deallocation. – Ben Nov 09 '20 at 18:31

1 Answers1

2
~NonThrowingDtor() noexcept try {
        x.willThrow = false;
    } catch (...) { 
        // Ignore because we know it will never happen.
    }

is "wrong" as equivalent to

~NonThrowingDtor() noexcept try {
        x.willThrow = false;
    } catch (...) {
        throw;
    }

so simply

~NonThrowingDtor() noexcept
{
    x.willThrow = false;
}

To not propagate exception, you have to use explicitly return:

~NonThrowingDtor() noexcept try {
        x.willThrow = false;
    } catch (...) { 
        return; // Required to not propagate exception.
    }

Unfortunately, msvc still warns with this form which doesn't throw.
(On the other side, clang/gcc don't warn for implicit (but do for explicit) throw in that case).

Jarod42
  • 203,559
  • 14
  • 181
  • 302