A Hierarchy of Errors

In Monday’s post, “Out-of-Band Error Reporting Without Exceptions”, I described a method for elegantly obtaining three out of five major advantages of C++ exceptions without actually using them. Now I’ll tell you how to go about grabbing a fourth advantage: exception type hierarchies.

C++ exceptions are hierarchical, and their hierarchy is a type hierarchy such that a more-derived exception can be treated, if desired, as one of its base exception types. For example, a FileNotFoundException might be derived from FileIOException. Some code may choose to handle FileNotFoundExceptions distinctly from other FileIOExceptions. Other code might delegate all FileIOExceptions to a common handler. In C++ this is done with implicit typecasting in the catch statements in a try block.

Wouldn’t it be useful if we could do something similar with the ErrorReport and CheckedValue classes that were defined in Monday’s post, instead of just having an error/not-an-error indicator and a human-readable message? Well, we can. It’s easy to use but a little tricky to set up.

Our goal, to be clear, is to essentially turn something like this:

std::string contents;
try {
  contents = readFile("foo");
  std::cout << contents << std::endl;
} catch (FileNotFoundException ex) {
  // Handle File-Not-Found
} catch (FileIOException ex) {
  // Handle other File I/O problem
}

into something like this:

CheckedValue chkContents = readFile("foo");
if (!chkContents.isError()) {
  std::cout << *chkContents << std::endl;
} else if (chkContents.isError(Errors::FileNotFound)) {
  // Handle File-Not-Found
} else if (chkContents.isError(Errors::FileIO)) {
  // Handle other File I/O problem 
}

Such that, with the only difference being a lack of stack-unwindning in the second example, the two constructs should behave identically.

The first thing to rule out is the idea of making the error identifiers an enumerated value. Implementing hierarchical organization on an enumeration would take a jumble of if-else logic and would make it very difficult to extend the pattern by defining new error types within the hierarchy. Instead, we’re going to let the C++ type system take care of this for us.

Now, the straightforward way to do that might seem to be to define the error types as a straight type hierarchy and use dynamic_cast to determine if one error type is derived from another. But then remember what dynamic_cast does when the casn’t doesn’t work: it throws a std::bad_cast exception, and exceptions are exactly what we want to avoid here.

What we are going to do is give each error a unique name (a string), which will normally, although not necessarily, be the same as the name of its associated class. Then, each error type (e.g. Errors::FileNotFound will be a pointer to an object of an associated class. The class will have an isDerivedFrom method, which (assuming they it is implemented properly) will allow us to easily, and without using dynamic_cast determine if one error is logically derived from another.

All of this, of course, must begin from a mother-of-all-errors class, which we will call Error.

namespace ErrorClasses {
  class Error {
    public:
    bool isDerivedFrom(const Error* err) const 
      { return isDerivedFrom(err->name); }
   
    protected:
    Error() : name("Error") {}
    virtual bool isDerivedFrom(const std::string& name) const 
      { return (this->name == name); }

    const std::string& name;
  };
}

Note first of all that this class is in the ErrorClasses namespace, rather than the Errors namespace that I used in the initial example with the try block. This is deliberate. The Errors namespace will be reserved for the pointers to these objects that will be given to ErrorReport and CheckedValue objects. Next, note that the constructor is protected. This prevents construction of base errors, but you could change this if you like. Finally, note that the public method is non-virtual and calls a virtual protected method, which will be overriden in the same way by each derived class.

Each derived class will have the following form:

namespace ErrorClasses {
  class FooError : public Error {
    public:
      FooError() : name("FooError") {}
    protected:
      virtual bool isDerivedFrom(const std::string& name) const
        { return (this->name == name) || Error::isDerivedFrom(name); }
      const std::string& name;
  };
}

You can even set up a macro to help out with this:

#define DEF_ERROR(ErrorName, ParentError) 
  class ErrorName : public ParentError { 
    public: 
      ErrorName() : name(#ErrorName) {} 
    protected: 
      virtual bool isDerivedFrom(const std::string& name) const 
        { return (this->name == name) ||  
                  ParentError::isDerivedFrom(name); } 
      const std::string& name; 
  }

When defined like this, you can see how the derivation check works. When a user calls isDerivedFrom on one error, passing another, the non-virtual, public version of the method extracts the string name from the passed error, and calls the virtual, protected version of isDerivedFrom. The way this is implemented, the most-derived class checks if the name to compare against is equal to its own name, and if not, recursively asks its parent class if it is derived from that name. This proceeds recursively until either we match a class name, or we reach the mother-of-all-errors superclass, which does not perform the recursive step.

Finally, to complete this system, we must simply declare and define a canonical error pointer in the Errors namespace for each error, like so:

// In a header file
namespace ErrorClasses {
  DEF_ERROR(FileIOError, Error);
  DEF_ERROR(FileNotFound, FileIOError);
  DEF_ERROR(FilePermissionDenied, FileIOError);
}

namespace Errors {
  using ErrorClasses::Error;
  const Error* FileIOError;
  const Error* FileNotFound;
  const Error* FilePermissionDenied;
}

// In a source file
namespace Errors {
  extern const Error* FileIOError = new ErrorClasses::FileIOError();
  extern const Error* FileNotFound = new ErrorClasses::FileNotFound();
  extern const Error* FilePermissionDenied = new ErrorClasses::FilePermissionDenied();
}

Now we have a complete hierarchy of errors that we can extend and query without using exceptions or complicated branching logic. These can now be used in ErrorReport objects (and by extension in CheckedValue objects), by changing the ErrorReport constructor that takes the description string to also take a const Error* describing the error type, and by overloading the ErrorReport::isError method to optionally accept a const Error*, with the semantics being that this version of isError returns true if and only if the contained error is equal to, or derived from, the supplied error.

One important note is that in order to completely mimic the behavior of the try block, ErrorReport::isError should not set its checked flag to true unless it actually returns true. We still want an ErrorReport to “do something horrible” if we checked some series of error types that it wasn’t but never actually found out what it was.

For demonstration purposes, let’s take a look at how we might write the final example from Monday using this new feature:

CheckedValue<int> computeSomething(Foo* p_foo)
{
  if (p_foo == NULL) {
    return ErrorReport(Errors::NullPointer,
                       "p_foo cannot be NULL");
  }
  int computed_value;
  // Compute Something
  return computed_value;
}

int main() 
{
  Foo* p_foo = new Foo();
  CheckedValue<int> chkVal = computeValue(p_foo);
  if (chkVal.isError(Errors::NullPointer)) {
    // Handle Null Pointer error
  } else if (chkVal.isError())  {
    // Handle some other kind of error
  } else {
    std::cout << *chkVal << std::endl;
  }
  delete p_foo;
  return 0;
}

Simple to use, and provides much greater flexibility.

Incidentally, this also provides a neat solution for the question of what to do if someone tries to construct a CheckedValue with an ErrorReport object that contains no error – simply create some sort of UninitializedCheckedValue error, and have the CheckedValue constructor set itself to containing that error if indeed it is passed a no-error error report.


Share this content on:

Facebooktwittergoogle_plusredditpinterestlinkedinmailFacebooktwittergoogle_plusredditpinterestlinkedinmail

Leave a Reply

Your email address will not be published. Required fields are marked *