I've reshaped my exception handling module to something that seems to me is more manageable. But I hit a serious obstacle.
The new exception handling framework builds on top the standard library exceptions. Two distinct hierarchies make up the framework.
- The standard exceptions rooted in the std::exception class; nothing new there.
- And the Mediaeval exception hierarchy rooted in the mediaeval_exception class.
The purpose of the mediaeval_exception class and its derived classes differs from the standard library in that it attempts to describe exceptions in the context of the application modules and some of the main objects being used (objects that, because of their complexity, deserve this honor).
Meanwhile because this context doesn't necessarily preclude the logic behind the standard exception hierarchy, at the lowest level of granularity, its classes also derive from either std::runtime_error or std::logic_error where appropriate. I believe this way both contexts are served.
Follows a snippet of the structure:
Code:
#include <stdexcept>
namespace mediaeval {
namespace error {
class mediaeval_exception { // TOP CLASS
public:
explicit mediaeval_exception(const char *err): cstr_(err) {}
virtual ~mediaeval_exception() throw() {}
virtual const char* what() const throw() { return cstr_; }
private:
const char * cstr_;
};
//Base class of a module related branch
class database_error: public mediaeval_exception {
public:
explicit database_error(const char *err)
: mediaeval_exception(err) {}
virtual ~database_error() throw() {}
};
//Base class of a Unit object related branch
class unit_error: public mediaeval_exception {
public:
explicit unit_error(const char *err): mediaeval_exception(err) {}
virtual ~unit_error() throw() {}
};
// One of the database module related exceptions
class data_corruption: public database_error, public std::runtime_error {
public:
explicit invalid_database(const char *desc)
: database_error(desc), std::runtime_error(desc) {}
/* MI ambiguity resolution. */
virtual const char* what() const throw()
{ return mediaeval_exception::what(); }
};
// One of the Unit object related exceptions
class invalid_unit: public unit_error, public std::logic_error {
public:
explicit invalid_unit(const char *desc)
: unit_error(desc), std::logic_error(desc) {}
/* MI ambiguity resolution. */
virtual const char* what() const throw()
{ return mediaeval_exception::what(); }
};
} // namespace error
} // namespace mediaeval
The problem is this:
Base branch classes (like unit_error and database_error above) can also be thrown. As can (in theory) the TOP mediaeval_exception. This is so because I would rather prefer not to overpopulate the hierarchy. As such some errors, because of their rarity or general nature aren't expressed in terms of a leaf class. For instance, I don't have a self_reference error for Unit objects. If a unit tries to hit itself I throw unit_error("Unit tries to hit self.").
I wanted to have mediaeval_exception (the TOP class) inherit virtually from std::exception. Only that way I could guarantee that a user catching a std::exception& object would also catch any mediaeval_exception errors.
You are probably already grinning at this point. And rightly so.
I can't do that. Sadly, std::logic_error and std::runtime_error don't inherit virtually from std::exception.
Sometimes I feel this mortally wounds my framework. Other times I think it is clearly defined enough so that a user knows they must not rely solely on std::exception& just because I happen to derive the leaf classes from the standard library.
What do you think? Should I extend my framework to define all exceptions and make the base classes ABCs? Or is there another way? Or shouldn't I worry with that since the framework speaks for itself? Lastly, should I even be using MI on those leaf classes? Sometimes I also feel this exactly what is encouraging the problem and connecting the two hierarchies isn't really that of an advantage of even desirable.