This is an archive of the discontinued LLVM Phabricator instance.

Correct handling of the 'throw()' exception specifier in C++17.
ClosedPublic

Authored by jyknight on Nov 9 2021, 2:01 PM.

Details

Summary

Per C++17 [except.spec], 'throw()' has become equivalent to
'noexcept', and should therefore call std::terminate, not
std::unexpected.

Diff Detail

Event Timeline

jyknight requested review of this revision.Nov 9 2021, 2:01 PM
jyknight created this revision.
Herald added a project: Restricted Project. · View Herald TranscriptNov 9 2021, 2:01 PM
Herald added a subscriber: cfe-commits. · View Herald Transcript
rsmith added a subscriber: rsmith.EditedNov 9 2021, 3:45 PM

What's the motivation for this change? I believe the current behavior is still conforming: set_unexpected is no longer (officially) part of the standard library (though it still exists as a zombie name), and the default unexpected handler calls terminate, so calling unexpected rather than terminate should have the same effect, except in non-conforming programs that call std::set_unexpected anyway (and for such programs, calling unexpected seems like the behavior the programmer would expect). Do we generate better code if we call terminate rather than unexpected?

What's the motivation for this change? I believe the current behavior is still conforming: set_unexpected is no longer (officially) part of the standard library (though it still exists as a zombie name), and the default unexpected handler calls terminate, so calling unexpected rather than terminate should have the same effect, except in non-conforming programs that call std::set_unexpected anyway (and for such programs, calling unexpected seems like the behavior the programmer would expect). Do we generate better code if we call terminate rather than unexpected?

Today: no, we don't.

But, I'm planning to propose further changes to improve noexcept codegen in Clang -- which is currently quite bad, compared to what it should be, because it's effectively following the same rules as legacy throw(). This change allows those future optimizations to apply to throw() as well, in C++17 mode, which is the desirable outcome.

Peanut gallery says: It seems like Proto->canThrow() is already returning the correct answer for C++17-and-later: a function declared throw() cannot throw. So from the POV of C++17-and-later, this could be a simple patch:

- if (isNoexceptExceptionSpec(EST) && Proto->canThrow() == CT_Cannot) {
+ if (Proto->canThrow() == CT_Cannot) {

and the only reason we can't do this is because C++14-and-earlier needs a different behavior.

Meanwhile, it looks as if every other use of isNoexceptExceptionSpec (or its inverse isDynamicExceptionSpec) is related to pretty-printing the exception specification, and isn't used for semantics at all.
Perhaps a cleaner way to preserve the current special case for C++14-and-earlier is to add a "language version" parameter to canThrow, and/or introduce an isEssentiallyNoexcept() helper:

- if (isNoexceptExceptionSpec(EST) && Proto->canThrow() == CT_Cannot) {
+ if (Proto->isEssentiallyNoexcept(getLangOpts())) {
rsmith accepted this revision.Nov 10 2021, 10:55 AM

This change allows those future optimizations to apply to throw() as well, in C++17 mode, which is the desirable outcome.

I see. It seems inconsistent that throw(X) would still call unexpected but that throw() would call terminate, but I suppose in the throw() case there is really not much interesting that an unexpected_handler can do other than (take some logging action and) terminate the program -- in particular, it can't do exception translation. So maybe the inconsistency is not a big deal, and it's more important to get the better code generation, especially given how rare throw(X) is compared to throw(). OK, I think I'm convinced that this is the best direction.

clang/lib/CodeGen/CGException.cpp
479–484

Maybe the logic would be clearer if we swap the terminate and unexpected cases:

if  (EST == EST_Dynamic || (!getLangOpts().CPlusPlus17 && EST == EST_DynamicNone)) {
  // Prepare to call unexpected
} else if (Proto->canThrow() == CT_Cannot) {
  // Prepare to call terminate
}

This would keep the syntactic checks of EST separated from the semantic checks of canThrow.

This revision is now accepted and ready to land.Nov 10 2021, 10:55 AM

This change allows those future optimizations to apply to throw() as well, in C++17 mode, which is the desirable outcome.

I see. It seems inconsistent that throw(X) would still call unexpected but that throw() would call terminate, but I suppose in the throw() case there is really not much interesting that an unexpected_handler can do other than (take some logging action and) terminate the program -- in particular, it can't do exception translation. So maybe the inconsistency is not a big deal, and it's more important to get the better code generation, especially given how rare throw(X) is compared to throw(). OK, I think I'm convinced that this is the best direction.

Well, "throw(X)" is not even permitted by C++17. In Clang, we allow that it only if you turn off the default-error warning option. Given that, I'm not too worried about it keeping the legacy behavior, while "throw()" gets the new behavior.

clang/lib/CodeGen/CGException.cpp
479–484

Agreed, that's better. I'll make that change and submit.