I'm designing an modular exception class hierarchy to use in various projects. I intend to inherit from std::exception
in order to be maximally compatible with any exception-handling code. A design goal is that each exception's what()
method returns a string which contains a base message, which is dependent on the object's most specific class (i.e. equal for all objects of the class), and an optional instance-specific details message which specifies the origin of the exception.
The two main goals are ease of use (as in when throwing the exceptions), as well as ensuring that writing another exception subclass is as simple and repetition-free as possible.
Base class
The base exception class I wrote is the following. It is conceptually an abstract class, but not syntactically, since I don't have any virtual method to make pure virtual. So, as an alternative, I made all constructors protected.
/**
* Base class for all custom exceptions. Stores a message as a string.
* Instances can only be constructed from within a child class,
* since the constructors are protected.
*/
class BaseException : public std::exception {
protected:
std::string message; ///< message to be returned by what()
BaseException() = default;
/**
* Construct a new exception from a base message and optional additional details.
* The base message is intended to be class-specific, while the additional
* details string is intended to be instance-specific.
*
* @param baseMessage generic message for the kind of exception being created
* @param details additional information as to why the exception was thrown
*/
BaseException(const std::string &baseMessage, const std::string &details = "") {
std::ostringstream oss(baseMessage, std::ios::ate);
if (not details.empty()) oss << " (" << details << ')';
message = oss.str();
}
public:
/// `std::exception`-compliant message getter
const char *what() const noexcept override {
return message.c_str();
}
};
The intention of the above design is that any subclass of BaseException
defines a constructor that passes a class-specific base message (as the baseMessage
paramter) and an optional detail-specifier (as the details
parameter) as arguments to BaseException
's constructor.
Errors & warnings
Since I want to be able to differentiate between general exception "types", e.g. errors vs. warnings, I've made the following two virtually-inherited bases:
class Error: public virtual BaseException {};
class Warning : public virtual BaseException {};
Examples
Here are some (project-specific) examples of implementing concrete exception subclasses with this design:
/// Indicates that a command whose keyword is unknown was issued
class UnknownCommand : public Error {
public:
static constexpr auto base = "unrecognized command";
UnknownCommand(const std::string &specific = "") : BaseException(base, specific) {}
};
/// Indicates an error in reading or writing to a file
class IOError : public Error {
public:
static constexpr auto base = "unable to access file";
IOError(const std::string &specific = "") : BaseException(base, specific) {}
};
/// Indicates that an algorithm encountered a situation in which it is not well-defined;
/// i.e., a value that doesn't meet a function's prerequisites was passed.
class ValueError : public Error {
public:
static constexpr auto base = "invalid value";
ValueError(const std::string &specific = "") : BaseException(base, specific) {}
};
# [...]
As you can see, the common pattern is
class SomeException : public Error /* or Warning */ {
public:
static constexpr auto base = "some exception's generic description";
SomeException(const std::string &details) : BaseException(base, details) {}
}
Usage example
Taking the previous IOError
class as an example:
#include <iostream>
#include <fstream>
#include "exceptions.h" // all of the previous stuff
void cat(const std::string &filename) {
std::ifstream file(filename);
if (not file.is_open()) throw IOError(filename);
std::cout << file.rdbuf();
}
int main(int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
try { cat(argv[i]); }
catch (std::exception &e) { std::cerr << e.what() << '\n'; }
}
}
In case of calling the program with an inaccessible file path, e.g. the path "foo", it should output
unable to access file (foo)