[contract] noexcept and throwing handlers
Hello all, I would like to discuss contracts and exception specifications (e.g., noexcept) specifically with respect to what Boost.Contract does. On one hand, it seems silly to allow a contract failure handler to throw an exception to only translate such an exception into a call to terminate() for noexcept functions. However, noexcept can be considered part of the function contract (namely, the contract that says the function shall not throw) and contract failure handlers should not be allowed to break such contract, not even when they are allowed to throw. For example consider a noexcept function fclose() that is called from a destructor without a try-catch statement (correctly so because fclose is declared noexcept). If fclose() is now allowed to throw when its preconditions fail, that will cause the destructor ~x() to throw as well?! void fclose(file& f) noexcept [[requires: f.is_open()]] ... class x { ~x() noexcept { fclose(f_); // f_ might not be open because of some bug... ... } file f_; }; (A point similar to the above is also made in N4160, in contrast with what argued in N4110.) Functions should honor their exception specifications (noexcept, etc.) even when their contracts fail and contract failure handlers are configured to throw. That is what Boost.Contract does (given that contract checking code appears within the function definition so it is subject to the exception specifications that appear in the enclosing function declaration): void fclose(file& f) noexcept { // This will call terminate()... boost::contract::guard c = boost::contract::function() .precondition([&] { BOOST_CONTRACT_ASSERT(f.is_open()); // ...even if this throws. } ; ... } int main() { boost::contract::set_precondition_failure( [] (boost::contract::from where) { // Re-throw exceptions (assertion_failure, user-defined, etc.). if(where != boost::contract::from_destructor) throw; std::terminate(); // But destructors never throw. } ); ... } Also note that a contract framework that allows to install failure handlers that throw exceptions should also pass some sort of parameter to the handler function to indicate if the contract failure happened in a destructor or not. That is so the failure handlers can choose to never throw for destructors, not even if class invariants (or postconditions) fails when checked by destructors. --Lorenzo
On Aug 5, 2016, at 2:13 PM, Lorenzo Caminiti
I would like to discuss contracts and exception specifications (e.g., noexcept) specifically with respect to what Boost.Contract does.
I’m not following C++ standards development closely, but perhaps my comments may be of some use.
However, noexcept can be considered part of the function contract (namely, the contract that says the function shall not throw)
For example consider a noexcept function fclose() that is called from
I’m going to use POSIX’s close() as an example in this discussion.
a destructor without a try-catch statement (correctly so because fclose is declared noexcept). If fclose() is now allowed to throw when its preconditions fail, that will cause the destructor ~x() to throw as well?!
If passed -1 as an argument, close() will set errno = EBADF and return -1. This is documented behavior. A function that throws an exception when passed -1 (or under any other circumstance) is not the close() function from POSIX and should be given a different name (if it’s declared in the global namespace, at least).
void fclose(file& f) noexcept [[requires: f.is_open()]]
I don’t see why this should be treated differently than void fclose(file& f) noexcept { if ( ! f.is_open() ) throw failed_precondition(); The purpose of noexcept, as I understand it, is to ensure that a called function will under no circumstances throw an exception, sparing the caller the need to wrap it in try/catch. Adding a loophole that allows compiler-provided glue to throw, even when the function itself strictly speaking doesn’t, invalidates this guarantee and undermines the utility of noexcept, in my opinion. If the compiler can statically prove that the precondition is always satisfied, fine. But if not, then I’d prefer that the above code not compile unless `noexcept` is removed. Josh
On Fri, Aug 5, 2016 at 6:57 PM, Josh Juran
On Aug 5, 2016, at 2:13 PM, Lorenzo Caminiti
wrote: I would like to discuss contracts and exception specifications (e.g., noexcept) specifically with respect to what Boost.Contract does.
I’m not following C++ standards development closely, but perhaps my comments may be of some use.
However, noexcept can be considered part of the function contract (namely, the contract that says the function shall not throw)
For example consider a noexcept function fclose() that is called from
I’m going to use POSIX’s close() as an example in this discussion.
a destructor without a try-catch statement (correctly so because fclose is declared noexcept). If fclose() is now allowed to throw when its preconditions fail, that will cause the destructor ~x() to throw as well?!
If passed -1 as an argument, close() will set errno = EBADF and return -1. This is documented behavior. A function that throws an exception when passed -1 (or under any other circumstance) is not the close() function from POSIX and should be given a different name (if it’s declared in the global namespace, at least).
POSIX close() has a "wide contract" (i.e., no preconditions). For my example, I made up my own fclose() that has a narrow contract instead (i.e., it has one or more preconditions). The fclose() I use in my example is not POSIX close(). Don't worry about POSIX close(), just assume there's some sort of fclose() defined as I stated it for the sake of the example I am illustrating.
void fclose(file& f) noexcept [[requires: f.is_open()]]
I don’t see why this should be treated differently than
void fclose(file& f) noexcept { if ( ! f.is_open() ) throw failed_precondition();
For example, this strategy will not work for class invariants failing at destructor entry because it will make the destructs throw (which is not a good idea in C++, and in fact destructors are implicitly declared noexcept in C++11): class x { bool invariant() const { ... } ~x() { if(!invariant()) throw failed_invariants(); ... This topic has been discussed in great length for C++... quoting N4160 (but also N1962, P0380, many other contract proposal for C++, previous emails on this list, etc.): ``What can a broken contract handler do? The most reasonable default answer appears to be std::terminate, which means "release critical resources and abort". One may wish to override the default in order to do special logging. We believe that it is not a good idea to throw from a broken contract handler. First, throwing an exception is often an action taken inside a function,in a situation where it cannot satisfy the postcondition, to make sure that class invariants are preserved. In other words, if you catch an exception or are in the middle of stack unwinding, you can safely assume that all objects' invariants are satisfied (and you can safely call destructors that may rely on invariants). If it were possible to throw from the handlers, his expectation would be violated. ...'' The above fclose() contract actually expands to something that calls a "precondition failure handler" functor: void fclose(file& f) noexcept { if (!f.is_open()) preconfition_failure_handler(from_function); ... Where by default the handler terminates: precondition_failure_handler = [] (from) { std::terminate(); }; But programmers can redefine it to throw (as you suggested above, but beware of what N4160 points out plus on how to program a throwing entry_invariant_failure_handler that shall not throw when from == from_destructor): precondition_failure_handler = [] (from) { throw failed_precondition(); }; Or to log and exit: precondition_failure_handler = [] (from) { some-logging-code; exit(-1); }; Or to take any other action programmers wish to take on precondition failure. Note: Boost.Contract does something a bit more complex than the above (using set/get functions to not expose the handler functor directly to the users, try-catch statements to call the handlers, etc.). These details are omitted here for simplicity.
The purpose of noexcept, as I understand it, is to ensure that a called function will under no circumstances throw an exception, sparing the caller the need to wrap it in try/catch. Adding a loophole that allows compiler-provided glue to throw, even when the function itself strictly speaking doesn’t, invalidates this guarantee and undermines the utility of noexcept, in my opinion.
Yes, I agree. That is essentially the point I was trying to make and what Boost.Contract does--no loopholes.
If the compiler can statically prove that the precondition is always satisfied, fine. But if not, then I’d prefer that the above code not compile unless `noexcept` is removed.
While desirable, this is not possible in practice. 1. First, it'd be great if the preconditions could be checked statically, but in general some times the preconditions will be satisfied and some other times they will not and the compiler will simply not know. Even if contracts were added to the language (as per N1962, P0380, etc.), static analysis tools will be able to statically check preconditions only some times, and not all the times. 2. Second, note that the compiler (and most likely static analysis tools) will not know if the action to take on contract failure is to throw or not because they will only see a call to precondition_failure_handler(from_function) and not `throw failed_precondition()`. What such failure handler call does is configured at run-time by the programmers so probably not always deductible at compile-time by the compiler or static analysis tools. Thanks, --Lorenzo
On 8/08/2016 01:06, Lorenzo Caminiti wrote:
void fclose(file& f) noexcept { if (!f.is_open()) preconfition_failure_handler(from_function); ...
Where by default the handler terminates:
precondition_failure_handler = [] (from) { std::terminate(); };
But programmers can redefine it to throw (as you suggested above, but beware of what N4160 points out plus on how to program a throwing entry_invariant_failure_handler that shall not throw when from == from_destructor):
precondition_failure_handler = [] (from) { throw failed_precondition(); };
Or to log and exit:
precondition_failure_handler = [] (from) { some-logging-code; exit(-1); };
Or to take any other action programmers wish to take on precondition failure.
It is an error for a noexcept function to call a non-noexcept function outside of a try-catch block. It therefore follows that if precondition_failure_handler is callable in that manner, it must be noexcept, and therefore cannot throw. Otherwise something is fundamentally broken. Whether the caller is a destructor or not is irrelevant; this applies to any noexcept method. Any code that makes special cases for destructors (other than to treat them as noexcept even if not specified) is probably also erroneous. Having said that, there is a small subset of code where it is actually useful to throw from a destructor, though such code needs to be written carefully to avoid problems if called during an unwind.
On Sun, Aug 7, 2016 at 5:22 PM, Gavin Lambert
On 8/08/2016 01:06, Lorenzo Caminiti wrote:
void fclose(file& f) noexcept { if (!f.is_open()) preconfition_failure_handler(from_function); ...
Where by default the handler terminates:
precondition_failure_handler = [] (from) { std::terminate(); };
But programmers can redefine it to throw (as you suggested above, but beware of what N4160 points out plus on how to program a throwing entry_invariant_failure_handler that shall not throw when from == from_destructor):
precondition_failure_handler = [] (from) { throw failed_precondition(); };
Or to log and exit:
precondition_failure_handler = [] (from) { some-logging-code; exit(-1); };
Or to take any other action programmers wish to take on precondition failure.
It is an error for a noexcept function to call a non-noexcept function outside of a try-catch block.
I guess you could argue this is a programming error... in any case, the standard just says that std::terminate() will be called as the exception tries to escape the noexcept function.
It therefore follows that if precondition_failure_handler is callable in that manner, it must be noexcept, and therefore cannot throw. Otherwise something is fundamentally broken.
The precondition (or any other contract) failure will always result in a call to std::terminate() by the noexcept function even if the precondition_failure_handler (or any other contract handler) is programmed to throw, that's all.
Whether the caller is a destructor or not is irrelevant; this applies to any noexcept method.
True. I have used destructors just because they are a well known example of functions that should (almost always) be noexcept. The same reasoning applies to any other noexcept function.
Any code that makes special cases for destructors (other than to treat them as noexcept even if not specified) is probably also erroneous.
Yes, in fact you can say the following code is making sure to treat destructors noexcept even when they are not explicit specified noexcept (i.e., making sure destructors (noexcept or not) do not throw on contract failures even when the contract failure handlers are configure to throw exceptions for contracts that fail from functions other than destructors): entry_invariant_failure_handler = [] (from where) { if(where != from_destructor) throw entry_invariant_failure(); std::terminate(); } Thanks, --Lorenzo
participants (3)
-
Gavin Lambert
-
Josh Juran
-
Lorenzo Caminiti