[RFC] Clang: true `noexcept` (aka, defaults are often wrong, hardcoded defaults are always wrong)

In previous installments, we’ve discussed (aka, recommended reading)

… which was proposing to add a nounwind function attribute,
as part of fixing pure/const attributes that were previously erroneously implying it.
While that RFC was accepted, there has been a significant pushback
during implementation: ⚙ D138958 [clang] Better UX for Clang’s unwind-affecting attributes

Instead, let’s explore an alternative, more global solution:
What if we don’t introduce a new way to do the same thing differently,
but just fix the existing way of doing it? What if we simply make
noexcept not mean “abort on exception”, but “(sanitized) UB on exception”?
While obviously we can’t just change it for everyone,
it can be done via a new opt-in C++ dialect, namely -fstrict-noexcept.

Doing so sidesteps the usual woes of adding a new (expert-only) attribute,
and allows for much easier enablement story – a single flag is simpler
to add than spilling a new attribute everywhere.

Why might we want to do that, you might ask. The problem is, noexcept means well,
but falls short. While it strives to help with optimizations – knowing that something is
a simple call is really really important for good straight-line code, failure to do so,
and ending with an invoke, is really detrimental to many optimizations – we still end up
with an invoke even if we use noexcept: Compiler Explorer

How many times did someone intentionally concisely wrote such a code thinking
“yes, i really want to always to check that it does not throw, and abort otherwise”?
I’m sure not a zero times, but is that what everyone needs?
Sure, security-critical code may want that, but everyone?

Likewise, one could manually tell the compiler
that no exception happens: Compiler Explorer
Err, well, i guess you can’t? ¯\_(ツ)_/¯. Not the outcome i expected, TBH.

This question here is a just yet another manifestation of an always-question of
the defaults, with other examples being “what should we do with concepts?
do we always verify preconditions? or can we just optimize on the assumption
that they are true”, and “what should assert do? should it be always-on?
or can we optimize on the assumption that it is always true”, and
“__builtin_expect()/__builtin_unreachable() are footguns”.

In the end, such questions do not have a right answer. That is, assuming that
the developers implementing it are sufficiently non-homogeneous, and accept that
they don’t know better than everybody else. This proposes a low-cost solution to one of them.
One that is easy to implement, and does not cause language forking in the way zero-init did.

Are there any concrete reasons why this must not be done?
Thoughts?

Roman.

2 Likes

CC @AaronBallman @jcranmer @rjmccall @erichkeane @jyknight @zygoloid

Rough draft implementation: ⚙ D141561 [clang] True `noexcept` (`-fstrict-noexcept` language dialect)

So, noexcept doesn’t mean “abort on exception”, it means “call std::terminate” on exception, which is a user-replaceable behavior. I suspect it is done rarely though. That said, I think a flag like this is at least explainable, and easily documented.

I discussed this offline with Aaron, and I have MILD concern about throw(), which are the dynamic exception specifiers. Pre-C++17, this calls a std::unexpected_handler followed by std::terminate (with the same replaceable behavior above). In C++17, throw() means noexcept, and it is removed after that.

IF we decide to handle throw() the same way here though, we should ALSO be considering what we do with dynamic exception specifiers that are violated, such as:

void foo() throw(int) { throw 1.0; }

In Pre-C++17, that is legal code that should go through the std::unexpected_handler and std::terminate workflows.

SO what ever we decide to do here needs to document THAT in some way.

Thank you for this RFC!

Normally, we try to avoid language dialect compiler options in Clang whenever possible. However, we already have a much bigger language dialect in this same space with -fno-exceptions. That option exists because of the significant overhead that exceptions can have on programs that do not use exceptions at all. The option you’ve proposed is along the same lines; it exists because of the significant overhead of well-defined termination handling when an exception is thrown from a non-throwing interface.

In terms of dynamic exception specifications; I think it’s best to handle them the same way as a noexcept specification, especially because C++17 defined throw() as being equivalent to noexcept. I think the semantics of this flag are effectively “if an exception escapes a function that is marked such that the exception should not escape (including type mismatches for dynamic exceptions), you get UB instead of well-defined semantics”. This seems relatively easy to document and explain to users, but more importantly, it gives some very useful functionality users can’t easily get themselves (better optimization opportunities, sanitizer support).

If we’re worried about folks enabling this flag while having a termination or unexpected handler installed, we could try to diagnose calls to set the handler if the flag is enabled. It won’t be perfect (e.g., calling the handler through a function pointer), but it should at least catch the few folks who do that sort of thing.

So I think this flag is an idea worth exploring further.

1 Like

Thank you all for responses so far!

I do agree that dynamic exception specification is an interesting point.
I do believe that under -fstrict-noexcept, when not sanitizing for this UB,
we should treat is as a complete no-op, and not emit anything for it,
neither a call to std::unexpected_handler, nor a landingpad in the first place.

I’ve implemented that in the patch, although docs could use some work.

I’m not really sure about trying to diagnose calls to set the handler if the flag is enabled.
I think we can’t really do that in clang, as that will have false-positives that we can’t fix.

Roman

1 Like

I’ve previously suggested using attributes for this:

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=86286

That wouldn’t have to be a global setting that affects all functions, like -fstrict-noexcept would be.

Maybe it would be nice to have the attribute so that individual functions could be annotated that way, and then -fstrict-noexcept would just mean “treat very noexcept function as if it had the attribute”.

As described in the first post here, that’s what i proposed
in the previous RFC, but it got shut down during review.

With a global setting, you can just split the .cpp into multiple smaller files,
if you really don’t want some noexcept to be affected.

I think we don’t need this new language dialect – instead, we just need to generate efficient code for noexcept, as proposed by RFC: Add "call unwindabort" to LLVM IR (which I’m very sorry took me so long for me to post a patch for).

1 Like

Thanks for those patches! I agree that they are an improvement, but they are orthogonal here,
because they still retain the exception handling, since the defined side-effects remain the same,
since you still must get “abort” on unwinding out of nounwind, so we wouldn’t be able to eliminate
potential throws that we later know to cause abort, yet we can do that with this RFC.

So no, this RFC is not obsolete.

Is there a reason you need to allow users to mark individual functions as opposed to the entire TU?

(FWIW, I think changing the semantics of __attribute__((nothrow)) at this stage is kind of dangerous; I don’t know how many users would expect the silent semantic changes.)

See the unique ptr example in the GCC report. I want to disable the forced terminate for a function where it’s undefined to throw, but that’s inline code in a library header. I don’t want to impose that on all user code that uses it, just that bit of code. It might get used in a TU along with another function that does want to turn exceptions into terminate calls

Hm, now that does seem like a motivation-enough.
Sounds like i should re-add the nounwind attribute to the appropriate review? :slight_smile:

1 Like

Yeah, that sounds well-motivated to me as well.

Now the questions are: 1) do we modify __attribute__((nothrow)) to have the desired codegen properties or do we introduce a new attribute? Clang documents that attribute as being equivalent to marking the function noexcept, so I worry about silent changes to semantics. 2) Do we still want a TU-wide flag?

@jwakely – GCC’s docs don’t seem to claim any relationship to noexcept, is that accurate or are you considering changing the attribute’s semantics?

Certainly not. I mean, i don’t understand why it exists since there is noexcept already,
but the ship has sailed already. We can’t just add new UB to something well-established
as not having that UB.

I think we do. Originally i starter with an attribute because i thought it was an easier sell,
but a flag obviously has better ergonomics.

@jwakely to reword @AaronBallman’s question slightly.
Say we implement nounwind attribute, and -fstrict-noexcept.
Will gcc follow suit and mirror that, or will you diverge?

I came to the conclusion it should be a separate attribute.

I can’t say, I just work on the library, I don’t get to decide such things! I think it would be useful, but that doesn’t mean the compiler folks would agree.

I think introducing new UB should have strong motivation. The motivation initially described is strong – based on the code that Clang currently generates. But, we can fix that, and the more we fix that, the less strong the motivation becomes.

Considering jwakely’s example with GCC’s codegen: the only non-optimality there is an extra LSDA gcc_except_table gets emitted. We could save memory by not emitting that table. Yet, that also seems solvable in another way: we ought to be able to arrange to share a single such empty table across functions.

So, yes, I agree this RFC is not necessarily obsolete. But given the improvements in codegen we can achieve without it, I’m unconvinced that enough optimization opportunity will remain to make it still be worth the cost.

Is the following transform legal given unwindabort patches?