A blog about in-depth analysis and understanding of programming, with a focus on C++.

Sunday, July 22, 2007

Errors, Exceptions, and Limit Science

As it currently stands, there are basically two kinds of C++ programmers. The distinctions can be more subtle than those I'm about to describe, but it works well enough for these purposes.

First, there are the C++ programmers who pretend that C++ is really just C with classes. They're wary of any form of templates, RAII, and so forth. And exception handling is right out for these people. They solve problems as a programmer would with C, though they may dabble in some C++-isms. The STL is looked on with a wary eye, if for no other reason than that it does not do things the C way.

The other kind of C++ programmer are those more like myself. They see C++ as a fundamentally different beast from C. They solve problems in C++ by using C++ wherever reasonable. They tend to avoid C-isms like function pointers. Some of these people overindulge in certain aspects of C++, but in general, they're trying to use the language as it is meant to be used rather than merely taking the most obviously useful features and ignoring the rest.

And nowhere is the difference more clear than in the way that they deal with erroneous conditions. The C-in-C++ style programmer has only two real tools to handle error conditions: call exit() or some other form of immediate program termination (asserts, etc), or return an error code and let the user handle it.

Quick side note. The reason the C-in-C++ programmer refuses to use exceptions tends to be because they abhor RAII. You can't really use one without the other; you can't feel safe about letting exceptions unwind the stack unless you can guarantee that everything will get cleaned up. And you can't do that without embracing RAII fully. And then, if you're not throwing exceptions, you can't really use RAII, because exceptions are the only way to signal a constructor error. Thus it is a vicious circle; one must have either both or neither. "Do, or do not; there is no try."

In any case, the programmer who embraces C++ has more than just two options. Which is fortunate, as there are multiple different kinds of errors, and many C-style C++ doesn't recognize.

The first kind of error is the most obvious. Some piece of code has detected a condition so horrific that it feels that the program simply cannot continue. It must die, and do so immediately. It does not consult other code, and nobody has any way of preventing this. C handles this through exit(), but the C++ way is through std::terminate. In either case, they both mean the same thing: the program must die right now.

Of course, C++ didn't add std::terminate just because. You can actually set up a generic termination function that will be called by std::terminate. Now, this code really ought not throw any exceptions (or do things like allocate memory that might throw), because that's really bad inside of terminate. But outside of that, everything else is pretty much OK.

These kinds of errors are the kind you don't expect to get, but which may happen in the course of development. Asserts are another common way of dealing with them.

Next up is an error that is considered recoverable by the code that detects the error. What constitutes "recoverable" differs even from person to person (personally, I think throwing in the event of an out-of-memory error is absolutely wrong. The program should die immediately if not sooner). But in general, it is something that the code thinks that the calling code should be informed of.

C-style C++ has only one way of doing this: an error code. A return value from a function that the user code then detects and acts on.

There are basically two types of error codes: the kind that the calling code is supposed to ignore and the kind that it isn't. This corresponds to two basic subtypes of the recoverable error: "recover or die" and "recover or live".

What I mean by "recover or die" is that the calling code should either detect the error or the program's execution should halt. That is, the error, while being recoverable, requires action on the user's part. The error cannot be recovered from by pretending it didn't happen; someone external to the code must do something.

"Recover or live" is the opposite. It's OK if the user does not detect that the error happened. Everything is considered in a good enough state that the program can continue operating as normal. If the programmer wants to detect the error and do something, fine, but the program can continue as normal.

One big question a programmer has to decide when detecting a recoverable error is which kind it is. If you're writing a file, is it a recover-or-die or recover-or-live error? Technically, it could be the latter, but would a programmer appreciate that?

In any case, C-style C++ doesn't give the programmer the ability to handle "recover or die". After all, you can completely ignore any return value you wish, so error codes are meaningful only if the programmer using the code chooses to make them meaningful. The lower-level programmer has no way of forcing the higher-level code to handle the error.

In C++, exceptions provide a way to require testing the error code. However, there's another interesting way to force error testing that doesn't require some of the verbosity of exceptions. Exceptions allow any code in the callstack to handle the exception. The alternative method must be handled immediately at the call site. However, it does force the user to handle it, unlike error codes.

C-style C++ users do use this quite a bit themselves. Many functions that return a meaningful value (say, GetObjectType(x)) will return NULL in the event of a recoverable error. It is not a "recover-or-live" error, because the program will eventually reference NULL, thus causing death. But of course, this doesn't work on non-pointer types.

Thanks once again to the Boost library, we have Boost.Optional. Basically, a boost::optional<typename> object has either a (presumably valid) value of the typename or nothing. In order to use the optional value, the user must call a get function, which will assert in debug builds and do other unpleasant things in release builds. In either case, the user of the code is forced to at least think about the fact that the function may return nothing, since they have to store the boost::optional return value.

It is a weaker form than exceptions, but it is better and much less verbose in many cases. Particularly so if it is semantically OK for it to return nothing.

Real C++ style still can use errorcodes for "recover or live" errors. But it is good that C++ has ways of forcing the user to handle errors or the program will crash.