|
Boost : |
Subject: Re: [boost] [contract] noexcept and throwing handlers
From: Lorenzo Caminiti (lorcaminiti_at_[hidden])
Date: 2016-08-07 09:06:08
On Fri, Aug 5, 2016 at 6:57 PM, Josh Juran <jjuran_at_[hidden]> wrote:
> On Aug 5, 2016, at 2:13 PM, Lorenzo Caminiti <lorcaminiti_at_[hidden]> 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
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk