Transitioning to C++11 where destructors are implicitly declared with noexcept
Asked Answered
R

3

12

In C++11, a destructor without any exception specification is implicitly declared with noexcept, which is a change from C++03. Therefore, a code which used to throw from destructors in C++03 would still compile fine in C++11, but will crash at runtime once it attempts throwing from such a destructor.

Since there's no compile-time error with such a code, how could it be safely transitioned to C++11, short of declaring all and every existing destructor in the code base as being noexcept(false), which would be really over-verbose and intrusive, or inspecting each and every destructor for being potentially throwing, which would be really time-consuming and error-prone to do, or catching and fixing all the crashes at runtime, which would never guarantee that all such cases are found?

Roche answered 5/10, 2015 at 19:26 Comment(8)
Interesting it seems like the head revision of gcc provides a warning for this via -Wterminate. I don't see a similar option for clang.Overhear
Like I always said, C++ exception specification is a joke. Sorry, no constructive asnwer, but I share your pain.Stopwatch
@ShafikYaghmour That's interesting but my GCC 5.2.0 does not recognize the option. Do you have a reference for it?Allegra
@Allegra no reference, you can try it on wandbox I did a quick google search and nothing came up.Overhear
@ShafikYaghmour Yes, I did that web search too. Seems to be a coming feature in GCC 6. Are you already using that? It's very good news, anyway.Allegra
@Allegra no, not using it, I was just curious if either clang or gcc would warn. Seems like a nice to have.Overhear
Note that this feature only handles any immediate throws within the destructor itself. If you call a function which throws, it doesn't seem to detect that. Still, a very useful addition.Roche
@Stopwatch "C++ exception specification is a joke" No, Java exception spec are. C++ lets you write (and enforces) a guarantee that a function will not throw. Only confused ppl spreading various contradicting rants gave another impression.Theroid
A
10

Note that the rules are not actually that brutal. The destructor will only be implicitly noexcept if an implicitly declared destructor would be. Therefore, marking at least one base class or member type as noexcept (false) will poison the noexceptness of the whole hierarchy / aggregate.

#include <type_traits>

struct bad_guy
{
  ~bad_guy() noexcept(false) { throw -1; }
};

static_assert(!std::is_nothrow_destructible<bad_guy>::value,
              "It was declared like that");

struct composing
{
  bad_guy member;
};

static_assert(!std::is_nothrow_destructible<composing>::value,
              "The implicity declared d'tor is not noexcept if a member's"
              " d'tor is not");

struct inheriting : bad_guy
{
  ~inheriting() { }
};

static_assert(!std::is_nothrow_destructible<inheriting>::value,
              "The d'tor is not implicitly noexcept if an implicitly"
              " declared d'tor wouldn't be.  An implicitly declared d'tor"
              " is not noexcept if a base d'tor is not.");

struct problematic
{
  ~problematic() { bad_guy {}; }
};

static_assert(std::is_nothrow_destructible<problematic>::value,
              "This is the (only) case you'll have to look for.");

Nevertheless, I agree with Chris Beck that you should get rid of your throwing destructors sooner or later. They can also make your C++98 program explode at the most inconvenient times.

Allegra answered 5/10, 2015 at 20:7 Comment(1)
That was very useful, thanks! Since there was no real answer, though, I ended up answering myself; I wonder if you'd agree with my line of thought there.Roche
R
5

As 5gon12eder have mentioned, there are certain rules which result in a destructor without an exception specification to be implicitly declared as either noexcept or noexcept(false). If your destructor may throw and you leave it up to the compiler to decide its exception specification, you would be playing a roulette, because you are relying on the compiler's decision influenced by the ancestors and members of the class, and their ancestors and members recursively, which is too complex to track and is subject to change during the evolution of your code. Therefore, when defining a destructor with a body which may throw, and no exception specification, it must be explicitly declared as noexcept(false). On the other hand, if you are certain that the body may not throw, you may want to declare it noexcept to be more explicit and help the compiler optimize, but be careful if you choose to do this, because if a destructor of any member/ancestor of your class decides to throw, your code will abort at runtime.

Note that any implicitly defined destructors or destructors with empty bodies pose no problems. They are only implicitly noexcept if all destructors of all members and ancestors are noexcept as well.

The best way to proceed with the transition is therefore to find all destructors with non-empty bodies and no exception specifications and declare every one of them which may throw with noexcept(false). Note that you only need to check the body of the destructor - any immediate throws it does or any throws done by the functions it calls, recursively. There's no need to check destructors with empty bodies, destructors with an existing exception specification, or any implicitly defined destructors. In practice, there would not be that many of those left to be checked, as the prevalent use for those is simply freeing resources.

Since I'm answering to myself, that's exactly what I ended up doing in my case and it was not that painful after all.

Roche answered 5/10, 2015 at 22:50 Comment(2)
I think this is a good approach. When I write code, I always like being explicit and declare destructors as noexcept even if it isn't strictly necessary because it would be the default anyway. I'd like to point out though that you are not “leaving the decision to the compiler” if you're not explicit – these rules are written in the standard and the compiler has zero leeway to apply them. Safe for compiler bugs, of course.Allegra
I've actually had to revise the part where I recommended marking all destructors with non-throwing bodies noexcept, since their members and ancestors can still throw, and that would result in an abort if the destructor is marked noexcept. As for the rules, yes, those are very explicit - just hard to track by a human.Roche
I
3

I went through this same dilemma myself once.

Basically what I concluded is that, accepting the fact that those destructors are throwing and just living with the consequences of that is usually much worse than going through the pain of making them not throw.

The reason is that you risk even more volatile and unpredictable states when you have throwing destructors.

As an example, I worked on a project once where, for various reasons, some of the developers were using exceptions for flow control in some part of the project, and it was working fine for years. Later, someone noticed that in a different part of the project, sometimes the client was failing to send some network messages that it should send, so they made an RAII object which would send the messages in its destructor. Sometimes the networking would throw an exception, so this RAII destructor would throw, but who cares right? It has no memory to clean up so its not a leak.

And this would work fine for 99% of the time, except when the exception flow control path happened to cross the networking, which then also throws an exception. And then, you have two live exceptions being unwound at once, so "bang you're dead", in the immortal words of C++ FAQ.

Honestly I would much rather have the program terminate instantly when a destructor throws, so we can have a talk with who wrote the throwing destructor, than try to maintain a program with intentionally throwing destructors, and that's the consensus of the committee / community it seems. So they made this breaking change to help you assert that your destructors are being good and not throwing. It may be a lot of work if your legacy code base has lots of hacks in it but if you want to keep developing it and maintaining it, at least on C++11 standard, you are likely better off to do the work of cleaning up the destructors.

Bottom Line:

You are right, you can't really hope to guarantee that you find all possible instances of a throwing destructor. So there will probably be some scenarios where when your code compiles at C++11, it will crash in cases that it wouldn't under C++98 standard. But on the whole, cleaning up the destructors and running as C++11 is probably going to be a whole lot more stable than just going with the throwing destructors on old standard.

Indelicacy answered 5/10, 2015 at 19:34 Comment(8)
There is no consensus that throwing dtors are a bad idea. A few well known people does not make a consensus.Theroid
Ok, maybe not a consensus, but certainly a majority. I guess I can't speak for everyone, but the ones who aren't opposed to throwing dtors are so rare that I've never met someone who thinks it shouldn't be against coding standards, and I've never seen a library or software framework which relies on throwing dtors. I've only seen a bunch of legacy application code that does this.Indelicacy
The impression of a consensus is always exaggerated by group think and intimidation. The arguments against throwing dtors are very weak. The discussion comes down to "throwing dtors cause issues" (which is also true of MI), "throwing dtors lead to terminate" (which is also true of throw()), and in the "I don't want to think about the issue". Nobody wants to face the issue that dtors might throw because they cannot do their jobs, just as any other function. The issue of handling errors in dtors needs to be dealt with.Theroid
"I've never met someone who thinks it shouldn't be against coding standards" the same coding standard who say you must not use goto? Give me a break. These documents are almost always worse than useless. They enact rules but not ways to solve problems. Discussing throwing from a dtor is a bit silly if you don't discuss error handling. Exceptions are just tools.Theroid
"Exceptions are just tools." That's a good point, and in the same way, destructors are also just tools. Yet, I often read in coding standards, "use RAII idiom, and write exception-safe code", because that's part of the convention adopted by the project. Many of these standards consider a throwing dtor not to be exception safe, in the same way that using raw pointers for ownership is not exception safe -- if some code changes in the middle we could start leaking / crashing, if I had a raw pointer / stack allocated object with throwing dtor.Indelicacy
Indeed, it's "I don't want to think about the issue." It's easier, and at least in some programming styles, more idiomatic to avoid throwing dtors, because it avoids a bunch of crappy problems, gotchas, and maintainability issues, and it's not clear that you ever need a throwing dtor. It's not even clear that you need exceptions at all frankly.Indelicacy
"The issue of handling errors in dtors needs to be dealt with." Well, you are supposed to code in a style so that you avoid doing complex things in a destructor. Complex code that potentially fails should be invoked explicitly in a function, not silently during the course of exception handling for an unrelated issue. Otherwise you make things more complicated.Indelicacy
Let us continue this discussion in chat.Theroid

© 2022 - 2024 — McMap. All rights reserved.