Bug 61372

Summary: Add warning to detect noexcept functions that might throw [-Wnoexcept-mismatch]
Product: gcc Reporter: Ryan Johnson <scovich>
Component: c++Assignee: Not yet assigned to anyone <unassigned>
Status: UNCONFIRMED ---    
Severity: enhancement CC: dcrocker, egallager, rafael, rdiezmail-gcc, webrown.cpp
Priority: P3 Keywords: diagnostic
Version: 4.9.0   
Target Milestone: ---   
Host: Target:
Build: Known to work:
Known to fail: Last reconfirmed:
Bug Depends on:    
Bug Blocks: 87403    

Description Ryan Johnson 2014-05-30 16:40:40 UTC
The C++11 standard adds the "noexcept" specification that lets the programmer assert that a function does not throw any exceptions (terminating execution if that assertion ever turns out to be false at runtime). 

Unfortunately, there is currently no reliable way for a programmer to validate, at compile time, her assertion that a function does or does not throw. 

The closest thing is -Wnoexcept, which detects the (very narrow) case where the following all apply to some function F:
1. F lacks the noexcept declaration or has declared noexcept(false)
2. The compiler has determined that F cannot throw
3. F causes some noexcept operator to evaluate to false

Unfortunately, that narrow formulation makes it really hard to validate much of anything (see example and further discussion below).

It would be very helpful to have a warning flag which tells the compiler to report cases where a function's noexcept specification contradicts the compiler's analysis of the function body. Perhaps -Wnoexcept-mismatch={1,2,3}?

1 (high priority): functions declared noexcept(true) but which contain expressions that might throw. This validates stated noexcept assumptions, helping to avoid issues like PR #56166.

2 (medium priority): Also report functions declared noexcept(false) that in fact cannot throw (e.g. cases #1 and #2 for -Wnoexcept). This improves the accuracy of noexcept  validation, and also improves performance in general (by eliminating unwind handlers). And makes it easier to avoid/fix things like PR #52562. 

3 (low priority): Also report functions which lack any noexcept declaration but which cannot throw (similar to -Wsuggest-attribute for const, pure, etc.). This also improves accuracy of noexcept, but the programmer would have to decide whether to make the API change (marking the function noexcept) or whether it's important to retain the ability to throw in the future.

Probably none of the above warnings should be enabled by default, but it might make sense to enable -Wnoexcept-mismatch=1 with -Wall and -Wnoexcept-mismatch=2 with -Wextra.

To implement the warning, the compiler would make a pass over each function body (after applying most optimizations, especially inlining and dead code elimination). It would then infer a noexcept value by examining all function calls that remain, and compare that result with the function's actual noexcept specification (or lack thereof). No need for any kind of IPA: if a callee lies about its noexcept status, it's the callee's problem.

===========================
Workaround using -Wnoexcept
===========================

One might try to combine static_assert with noexcept, e.g:

// example.cpp ////////////////
void might_throw(int); // lacks noexcept
void also_might_throw(); // lacks noexcept
void never_throw(int a) noexcept(noexcept(might_throw(a)) && noexcept(also_might_throw())) {
    if (a)
        might_throw(a);
    also_might_throw();
}
void foo(int a) noexcept(noexcept(might_throw(a))) {
    might_throw(a);
}
static_assert(noexcept(foo(0)), "never_throw might throw");
////////////////////////////////

There are two glaring problems with that approach, however:

1. Every expression in the function body must be part of the noexcept clause, effectively replicating the function body in its signature (but without the ability to declare local variables).

   - Maintaining the noexcept chain across code changes would be tedious and error-prone for all but the smallest and most stable functions (= ie the ones least in need of verification).

   - Operator overloading means you can't even assume basic expressions like a+b are nothrow. To get complete coverage would require either a very careful analysis (error prone) or cracking the entire function body into an AST atomic expressions (tedious *and* error prone).

   - Macro expansions would add even more headaches, because they may expand to more than one statement and/or include control flow. 

2. The static_assert must choose one set of inputs for each function call it passes to operator noexcept. 

   - An optimizer (especially after inlining and constant propagation) could conceivably report that the function is noexcept for that particular input, when in fact other inputs exist that could cause an exception to be thrown (this does not seem to be the case currently). 

   - There may not be any easy way to come up with a valid input (objects that lack a default constructor, etc.). Using hacks like (*(T*)0) would violate all sorts of compiler/optimizer assumptions and risks breaking the analysis.

Another possibility would be to embed multiple static_assert in the function to verify noexcept status for every line of code. That would at least allow to use existing local variables, and resolve most of issue #2, but issue #1 remains. Code readability would take quite a hit, too.
Comment 1 David Crocker 2020-08-16 10:46:54 UTC
I too would find this very useful. However, it's essential that functions declared 'extern "C" ' are (or optionally can be) assumed to not throw exceptions. Otherwise, calls to C library functions and MCU vendor-supplied C header files from C++ functions declared 'noexcept' will trigger this warning. Also, the warning should be disabled when -fno-exceptions is used.
Comment 2 Jonathan Wakely 2020-08-17 09:42:47 UTC
extern "C" functions can throw, so it would be wrong to unconditionally assume they can't.
Comment 3 David Crocker 2020-08-18 14:03:22 UTC
(In reply to Jonathan Wakely from comment #2)
> extern "C" functions can throw, so it would be wrong to unconditionally
> assume they can't.

True, you can write an extern "C" function that throws. But does it ever make sense to? I don't think so. Functions written in C and then declared extern "C" in a C++ program so that they can be called from C++ won't throw or propagate exceptions, even if they end up calling functions written in C++ that throw. The only reason to write a function in C++ and declare it extern "C" is so that it call be called from C, in which case it had better not throw.
Comment 4 Jonathan Wakely 2020-08-18 14:46:08 UTC
(In reply to David Crocker from comment #3)
> (In reply to Jonathan Wakely from comment #2)
> > extern "C" functions can throw, so it would be wrong to unconditionally
> > assume they can't.
> 
> True, you can write an extern "C" function that throws. But does it ever
> make sense to? I don't think so. Functions written in C and then declared
> extern "C" in a C++ program so that they can be called from C++ won't throw
> or propagate exceptions,

That's not true. std::bsearch and std::qsort are counterexamples. You don't know what the user-provided function does, and you can't assume it doesn't throw.

> even if they end up calling functions written in
> C++ that throw. The only reason to write a function in C++ and declare it
> extern "C" is so that it call be called from C, in which case it had better
> not throw.

That's an incorrect assumption. Not all extern "C" functions are written in C++. Even if they are, you can't assume they don't throw because they could use callbacks that can throw, or they might throw even in C programs (which works fine for some targets, unwinding the stack exactly as a C++ program wants it to).

It would be OK to optionally assume functions with C language linkage don't throw, but it must be optional.
Comment 5 David Crocker 2020-08-19 14:05:01 UTC
You seem to be assuming that C++ exceptions can propagate through functions compiled with a C compiler. On most platforms, they normally cannot, because a C compiler does not produce the information needed for the C++ unwind mechanism to unwind the call stack. If you want bsearch and qsort to propagate C++ exceptions, on most platforms the implementations of those functions would have to be compiled either using a C++ compiler, or with a C compiler using a special option that tells the compiler to include the tables and/or code needed to propagate C++ exceptions. Otherwise the exception will end up at std::unexpected. Maybe bsearch and qsort in newlib are compiled this way, I haven't checked.

I don't care whether extern "C" functions are to be assumed noexcept by default or another compiler option is needed to specify that. But without this facility, the proposed warning will be useless in practice, at least for people like me writing embedded software.
Comment 6 Jonathan Wakely 2020-08-19 14:24:30 UTC
(In reply to David Crocker from comment #5)
> You seem to be assuming that C++ exceptions can propagate through functions
> compiled with a C compiler.

I'm not assuming anything. I know for a fact that they can do so on some platforms.

It is provably incorrect to say that extern "C" functions can't exit via an exception. So it would be wrong to add a new warning which unconditionally assumes extern "C" functions never throw.

> On most platforms, they normally cannot, because

Define most.

GCC supports it by default on many platforms.

> a C compiler does not produce the information needed for the C++ unwind
> mechanism to unwind the call stack. If you want bsearch and qsort to
> propagate C++ exceptions, on most platforms the implementations of those
> functions would have to be compiled either using a C++ compiler, or with a C
> compiler using a special option that tells the compiler to include the
> tables and/or code needed to propagate C++ exceptions. Otherwise the
> exception will end up at std::unexpected. Maybe bsearch and qsort in newlib
> are compiled this way, I haven't checked.

What has newlib got to do with anything? Are you asking for a new option that only works when using newlib? Don't assume your use case is universal. New GCC options need to consider a broad range of uses.
 
> I don't care whether extern "C" functions are to be assumed noexcept by
> default or another compiler option is needed to specify that. But without
> this facility, the proposed warning will be useless in practice, at least
> for people like me writing embedded software.

OK, so why do you keep arguing about it? What I said is it that treating extern "C" functions as non-throwing needs to be optional. You've repeatedly argued that my reasoning for that is bogus, based on "does it ever make sense to?" and the answer is yes, sometimes it does.
Comment 7 Andrew Pinski 2020-08-24 20:11:59 UTC
(In reply to Jonathan Wakely from comment #2)
> extern "C" functions can throw, so it would be wrong to unconditionally
> assume they can't.

Yes that is correct.  Even extern "C" functions could be written in C++ to provide an interface from C to C++ code.
Comment 8 Eric Gallager 2021-10-27 05:11:45 UTC
retitling to include proposed warning flag name in title
Comment 9 Eric Gallager 2021-10-27 05:17:26 UTC
*** Bug 94112 has been marked as a duplicate of this bug. ***