Exceptions, Go to Hell!

J

Joshua Maurice

When a library writer finds that a postcondition or invariant failure
has occurred in his code, it should be fixed. When a precondition
failure occurs in his code, he should throw an exception, there is no
way for him to "fix" it.

From what I see, the bigest question new programmers have about the
exception mechanism is about when to use it. They are told vague
generalities but not given anything concrete (they are told to use them
for "errors" and "exceptional situations".) I'm trying to clear out some
of the vagueness. As Stroustrup put it:

   ... the author of a library can detect run-time errors but does not
   in general have any idea what to do about them. The user of a library
   may know how to cope with such errors but cannot detect them ­ or
   else they would have been handled in the user¹s code and not left for
   the library to find. The notion of an exception is provided to help
   deal with such problems.

What Stroustrup is describing above are precondition violations.


Exceptions should not be (and in fact, cannot be) used for "all errors,"
unless you define "error" as a precondition violation.


To me, your two quotes above are equivalent, so neither is less correct
or less broad than the other.

I've been following this thread with a sense of dread, mostly because
I've seen this pop up several times and the same rehashed arguments
are given. Counter example to your claims:

1- Library should throw on every pre-condition violation. Sometimes
this is overly expensive or impractical to calculate. My favorite
example is the non-recursive mutex. Under "release mode" options, we
want it to be incredibly fast, and it is quite a noticable hit on some
systems to make it track this. Better yet, the system could detect
some deadlock conditions, but that would be incredibly expensive, so
again we do not want such deadlock detection turned on under "release
mode" options.

2- Libraries should not throw on post-condition violations. So, what
should we do when std::new fails to allocate memory? Or when a file
flush fails due to whatever reason (like the USB stick being
removed)?

What you say doesn't make sense unless you weaken all pre and post
conditions to drivel, such as weakening operator new to "Maybe it
allocates memory, maybe it doesn't".

I argue that "it depends" and I have yet to find a better succinct
rule. More specifically:

If you want fast code, then don't use exceptions on the code which is
required to be fast. If an error condition is not part of the fast
path, the set of use cases determined to have value, then exception
use may be warranted.

As more of a matter of an educated guess, if you want more
maintainable code, then if you have an error condition which is
unlikely to be handled at the caller, and is instead likely to be
handled much farther up the call stack, then use an exception.
 
J

Joshua Maurice

I've been following this thread with a sense of dread, mostly because
I've seen this pop up several times and the same rehashed arguments
are given.

I'm glad you joined in. Sorry that my arguments are rehashed, but I
certainly can't take credit for being the first person to use them. I am
walking in bigger shoes. :)
Counter example to your claims: [...]
2- Libraries should not throw on post-condition violations. So, what
should we do when std::new fails to allocate memory? Or when a file
flush fails due to whatever reason (like the USB stick being
removed)?

I think the problem here is in considering these to be post-condition
violations. It is not operator new's responsibility to ensure that the
requested memory is available, nor can operator new possibly know what
to do in such a case. [see my last comment below]
What you say doesn't make sense unless you weaken all pre and post
conditions to drivel, such as weakening operator new to "Maybe it
allocates memory, maybe it doesn't".

The post condition for operator new is that it allocates the memory and
returns a pointer to it. That's a pretty strong requirement, every bit
as strong as its precondition, that the required amount of memory be
available.
As more of a matter of an educated guess, if you want more
maintainable code, then if you have an error condition which is
unlikely to be handled at the caller, and is instead likely to be
handled much farther up the call stack, then use an exception.

Here you are saying the same thing that Stroustrup and I are saying, but
using different words. If the library can solve the problem then fine;
if it can't then the calling code must ensure that the issue never gets
to the library, i.e., it's a precondition for calling the library that
the issue not exist. Now the only question is what should the library
code do if it happens to detect that the precondition was not met?
terminate the app, throw an exception, or leave the behavior undefined.
Clearly, throwing an exception is the best choice.

Meh. That's an interesting way of putting it. It rubs me the wrong way
to say that operator new has a precondition that sufficient memory be
available. A precondition to me means something which the caller can
check for or code for. A nonrecursive mutex has the precondition that
it's not already owned by the current thread. The calling code can
ensure this precondition is met. A caller can in no way ensure that
"sufficient memory is available" when calling operator new (at least
not in any C++ standard way). Moreover, it's not practical to do so.
It's practical to meet the precondition for a nonrecursive mutex, but
it's prohibitively expensive in programmer time to ensure operator
new's precondition holds before calling it. I really do not like
calling that a precondition. I think that you're changing the meaning
of the term to suit your argument.

More generally, there are a class of things which can have their
preconditions met, but still fail. You don't like operator new, so how
about network calls? When I send a large chunk of data, it can confirm
at the front that the socket is still open, and every other
precondition possible. However, I can unplug the cord halfway through,
making the call fail. The network code can do nothing about it. That's
a postcondition violation, or you change its post conditions into the
drivel "may work, may not". The only sensible thing to do is return an
error code or throw an exception stating that it cannot fulfill its
post conditions.
 
J

Joshua Maurice

That is a pretty standard conception, but I don't think it is supported
well in the literature. For example:

   Meaning of a correctness formula: {P} A {Q}
   "Any execution of A, starting in a state where P holds, will
   terminate in a state where Q holds." (Meyer)

In the above, the precondition is P, the postcondition is Q and the
action to be performed is A. As Meyer tells us, this is not a
programming construct, it is a mathematical notation, a conceptual
framework within which to discuss the correctness of programming
constructs. If we allow 'operator new' to have as part of its
postcondition "or throws bad_alloc()", then we cannot call such an
action failure. An implementation of 'operator new' that throws
bad_alloc 10% of the time, even if memory is available would be working
correctly if this were the case.

Stroustrup reminds us that "Unfortunately, the assertions we would like
to make are often at a higher level than the programming language allows
us to express conveniently and efficiently." Yes it is difficult, if not
impossible, to check for adequate memory before calling operator new,
but that does not eliminate the assertion as a candidate for a
precondition.


I agree with your assessment above. Watering down the postcondition such
that code that does nothing can still be considered correct is not the
way to go. However, as I have shown above, the correctness formula
*requires* that the action must terminate with the postcondition true if
the precondition was met.

Remember, preconditions and postconditions are not just a correctness
criteria, they can also be used to determine who's at fault. If the
precondition is met, but the postcondition is not, then the action
itself is at fault. I think we can both agree that it would be wrong to
lay the blame on the writer of the network code for failing in the
situation you describe. If the precondition is not met, then something
outside the action is at fault, which we can agree is the case here.

I'm not sure if I'm agreeing. Your quote of Meyers says that
preconditions are restrictions on the state (of the universe) when the
action commences. In my example of the network cord being unplugged
halfway through, the preconditions are true when the call begins; the
network cord is plugged in. but the naive postcondition of "data is
sent" is false.

So, given your strict requirement that "Postconditions are met iff
code is correct and preconditions are met", then I think we have two
choices:

1- Redefine preconditions to include not just the state of the
universe at the time of entering the function, but to include the
state of the universe from the start of the function call to the end
of the function call, aka "The network must remain up for the duration
of the call."

2- Water down the postcondition to "maybe it sends it and returns a
'sent' error code (or exception), or maybe it doesn't send it and
returns an 'error' code (or exception)."

This is getting pretty pedantic. I want to conclude that the writer of
the network code is not at fault (and thus needs not be changed), the
writer of the code which calls the network code is not at fault (and
thus needs not be changed), and if the network cord is pulled, then
the network function call should return an error code or throw an
exception indicating that it could not and did not send all of the
data, which the caller should handle in an appropriate way. I'm not
sure I want to continue to argue over the definitions of pre and post
conditions and how we reach these conclusions, but these are the
conclusions which should be reached.

However, I think I'm getting where you're coming from. You want to be
able to prove the correctness of a program piecemeal. Each function
has preconditions, and if those preconditions are met then the
function should be guaranteed to succeed. I think it's a nice model
for some programs, but for other programs it is not. For the memory
subsystem, this depends on state outside the control of the program,
such as the current commit charge, whether overcommit is turned on,
etc. Same for the network. There are some parts of programs which
really don't fit that model which I think you're incorrectly
attempting to shoehorn into the model.
 
J

joe

(Aside: I so hate discussions about error handling that center around
pre/post-conditions and invariants).
... a library that doesn¹t know about the purpose and general
strategy of the program in which it is embedded cannot simply exit()
or abort(). A library that unconditionally terminates cannot be used
in a program that cannot afford to crash. (Stroustrup)

It is not up to the library code to make such high level decisions,
only the program's designer should be allowed to make such decisions.

Well I don't know how you grok "precondition", but I grok it as something
"assertable". Precondition violations cannot occur in released software
unless the software has a bug. The program should have been "provably
correct" (via testing and review) before release.

What you excerpted from Stroustrup was in the context of ERRORS, not
BUGS. Consider, that if a precondition violation has occurred, then the
program is defective, and hence has a BUG and the C++ exception machinery
was not created for that scenario. That machinery works very smoothly for
where it was intended to be used: as a contingency plan for things that
can conceivably prevent a function from suceeding.
I suggested that they are useful for informing users of your library
that they have a problem, since you can't fix it.

"users of a library" are PROGRAMMERS and hence that only happens during
DEVELOPMENT TIME. It is called debugging and testing. If you want to use
that heavyweight exception machinery for that is up to you. That's what
assertions are for. No need to be graceful and no need to recover during
development time: just throw the violation up at the developer's face
pronto and tell him to get his mind right and/or RTFM (F = Fine).

I don't think so.
Regardless, there is no single mechanism that can handle *all*
possible problems. Too much of C++ is left as "undefined behavior"
for that to be possible.

Non-sequitor, but this small section immediately above is not important.
If a function is called with correct preconditions, and it satisfies
its postcondition without breaking any invariants, then it can't be
said to fail;

Again, I am not ready to change my definition of error to one that
relates the term to pre/post-conditions and invariants. Indeed, they are
not even in the same realm as "error".
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,537
Members
45,020
Latest member
GenesisGai

Latest Threads

Top