Let’s say, for the sake of argument, that you’re working in C++ and you happen to be in an environment where exceptions don’t work. In my case, it was an embedded environment where the miniaturized C++ standard library didn’t support them, but in your case it may be that you can’t use them for performance reasons, or they’re against policy or something else.
As opposed to the old C-style return codes, C++ exceptions offer five major advantages:
- They are out of band (that is, a function returning an integer need not assign a special value, e.g. -1, to mean “error”).
- They can carry information about the specific error that occurred, as opposed to simply a code with a general meaning.
- They exist in a type hierarchy. A
FileNotFoundException
may be derived from aFileIOException
, and a handler for the latter can handle the former. - If you ignore them, your program blows up. This is a good thing – an undetected error is far worse than a fatal error.
- They automatically unwind the stack to find a handler.
And these are all reasons why, unless you have a good reason not to, you should prefer using exceptions. But in the case when you can’t, it turns out that point 5 is really the only advantage that you have to do without. There’s a relatively elegant method that you can use to achive the first four advantages without using exceptions at all.
First, let us ignore the “out-of-band” part, and focus on points 2 and 4. Carrying information, and being extremely noisy about being ignored. Consider first what you might return from a function that would otherwise return void, which would take the place of these two exception characteristics.
class ErrorReport { public: ErrorReport() : m_err(false), m_copied(false), m_checked(false) {} explicit ErrorReport(const std::string& message) : err(true), copied(false), checked(false), msg(message) {} ErrorReport(const ErrorReport& copy) : err(copy.err), copied(false), checked(copy.checked), msg(copy.message) { copy.copied = true; } ~ErrorReport() { if (err && !checked && !copied) { // Do something horrible } } bool isError() const { checked = true; return err; } std::string getMessage() const { return msg; } private: mutable bool checked; mutable bool copied; const bool err; const std::string msg; }
What this does should be fairly straightforward. It creates an error “report” which either is, or is not an error. If it is an error, it contains a message describing the error. It keeps track of whether it has been checked for being an error, and whether it has been copied. In a real implementation, you would probably want to implement the assignment operator as well.
On destruction, if it is an error or and has been neither checked nor copied, then it has been ignored, and it should “do something horrible”. For example, it should assert(false)
or call exit
or call some pre-defined handler, or just in general something that will make a whole lot of noise and let you know that you ignored an error.
You might use such a thing like this:
ErrorReport doSomething(Foo* p_foo) { if (p_foo == NULL) { return ErrorReport("p_foo cannot be NULL"); } // Do Something return ErrorReport(); // No error } int main() { if (doSomething(NULL).isError()) { // Handle error } // ... etc ... return 0; }
The only overhead involved is having to remember to return a default-constructed ErrorReport
from functions that would otherwise return void. This is fairly minor, and you’ll note that this method does indeed cover advantages 2 and 4 of exceptions.
But what about functions that do not return void? That’s where the out-of-band part comes in, and that’s where we cover advantage 1 of exceptions. All you need to do that is to have a class which either contains a return value, or is an error report. C++ templates allow this to be accomplished fairly easily:
template <typename T> class CheckedValue : public ErrorReport { public: CheckedValue(const T& value) : p_val(new T(value)) {} CheckedValue(const ErrorReport& er) : ErrorReport(er), p_val(NULL) {} CheckedValue(const CheckedValue<T>& copy) : ErrorReport(copy), p_val(NULL) { if (copy.p_val != NULL) p_val = new T(*(copy.p_val)); } ~CheckedValue() { delete p_val; } const T& operator*() const { return *p_val; } private: const T const* p_val; };
So what we have here is a derived type of an ErrorReport
which, if the ErrorReport
does not indicate an error, contains a value of some template-defined type. The value can be accessed through the dereference operator, which is familiar in C++ since it is the way that iterators behave in the STL. In real code, you might also want to define the indirection operator (->) such that it behaves as it does for iterators.
The internal copy of the value is heap-allocated for exactly one reason: so that T
is not required to be default-constructible. The internal value must have some value even if the CheckedValue
actually contains only an error report. If the internal value were not stored as a pointer, then it would have to be default-constructed, since there would be no other reasonable value to give it. The way it is done above, we only require that T
be copy-constructible, which is a much less onerous and more easily fulfilled requirement.
It inherits the public methods of ErrorReport
, including isError()
, which should of course be checked before attempting to access the value. Note that attempting to access the contained value of a CheckedValue
which is in fact an error will cause a NULL dereference, which is exactly the sort of “horrible” thing we want to happen when an error is ignored. Alternatively, you could detect this situation and handle it some other way (e.g. calling a handler), as with the ErrorReport destructor.
You would use such a thing like this:
CheckedValue<int> computeSomething(Foo* p_foo) { if (p_foo == NULL) { return ErrorReport("p_foo cannot be NULL"); } int computed_value; // Compute Something return computed_value; } int main() { Foo* p_foo = new Foo(); CheckedValue chkVal = computeValue(p_foo); if (chkVal.isError()) { // Handle Error } else { std::cout << *chkVal << std::endl; } delete p_foo; return 0; }
Using CheckedValue
s like this, error reporting is truly out-of-band. You can use the full range of your nominal return type as valid values. There is no need to designate specific value such as NULL or -1 or empty-string to indicate an error, nor is there need to take out-parameters by reference, which makes code more difficult to read.
Note that you never even have to explicitly construct a CheckedValue
. It can be implicitly constructed from a T
or from an ErrorReport
, so simply returning a report, or a value, will wrap it appropriately. Incidentally, this is why the constructor of ErrorReport
that takes a string must be declared explicit. Otherwise, construction of a CheckedReturn<std::string>
would be ambiguous.
Now there are two caveats with the above code that I should note. First, the choice to have the dereferencing operator return a const reference to the contained value is supposed to re-enforce that the semantics that contained object cannot be re-assigned. However, if T
is in fact a pointer type, returning a const T&
from the dereferencing operator is going to give you a const pointer, which is not what you want. Therefore, you will want to define a template specialization for pointer types which returns a non-const reference. Second, the CheckedValue
constructor that takes a ErrorReport
should ensure that the error report actually indicates an error. There are numerous ways you could modify the code to implement this. The situation where a CheckedValue
contains neither an error indication nor a valid value is something that should definitely be protected against.
These two classes relatively painlessly give you advantages 1, 2, and 4 of exceptions, without being exceptions. These features wholly replace in-band error reporting, and are much more elegant. But we can get closer to true exceptions still. What remains is to create a type-oriented hierarchy of errors, as opposed to a binary error / not-an-error choice when creating an ErrorReport
. This can be done, and can be elegant, but it is a bit complicated, and this post is already getting long. I will address it in a later post.
Share this content on:
1 comment