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)
1 Answer 1
Using a
std::ostringstream
to concatenate strings is like using a DeathStar to kill a sparrow.Don't use
std::string
for the stored message. Copying it is not guaranteed to never throw, and you only need a very small sliver of its capabilities anyway.
Astd::shared_ptr<const char[]>
fits the bill much better, though even that is overkill.Avoid using
std::string
at all, so you don't risk short-lived but costly dynamic allocations. Preferstd::string_view
for the interface.BaseException
seems to be purely an implementation-help, adding storing of an arbitrary exception-message on top ofstd::exception
. That's fine, only a pitty it wasn't already in the base.Still, marking the additions as
protected
doesn't make any sense,message
should really beprivate
, and why shouldn't the ctors bepublic
?
If the aim of that exercise is forbidding objects of most derived classBaseException
, just make the ctor pure virtual:// Declaration in the class: virtual ~BaseException() = 0; // Definition in the header-file: inline BaseException::~BaseException() = default;
Applying that:
template <class... Ts>
auto shared_message(Ts... ts)
-> std::enable_if_t<(std::is_same_v<Ts, std::string_view> ... &&),
std::shared_ptr<const char[]>> {
auto r = std::make_shared_default_init<char[]>(1 + ... + ts.size());
auto p = &r[0];
((std::copy_n(&ts[0], ts.size(), p), p += ts.size()), ...);
*p = 0;
return r;
}
template <class... Ts>
auto shared_message(Ts&&... ts)
-> std::enable_if_t<!(std::is_same_v<std::decay_t<Ts>, std::string_view> ... &&),
decltype(shared_message(std::string_view{ts}...))>
{ return shared_message(std::string_view{ts}...); }
class BaseException : std::exception {
decltype(shared_message()) message;
public:
const char* what() const noexcept final override
{ return &message[0]; }
virtual ~BaseException() = 0;
template <class... Ts, class = decltype(shared_message(std::declval<Ts&>()...))>
BaseException(Ts&&... ts)
: message(shared_message(ts...))
{}
};
inline BaseException::~BaseException() = default;
-
\$\begingroup\$ Thanks for your feedback! Conceptually speaking,
BaseException
is an abstract class -- although not syntactically, since there is no method to make pure virtual; so, as an alternative, I made the constructorsprotected
. Regarding the use ofshared_pointer
, a copy must still be made, to avoid dangling pointer issues, right? In this case, would the "manual" copy potentially throw? And would switching toshared_pointer
or a more "basic" string type be worth it if the thrower actually passes astd::string
as the "details" (so thestd::string
would already have been constructed)? \$\endgroup\$Anakhand– Anakhand2019年05月05日 09:39:40 +00:00Commented May 5, 2019 at 9:39 -
1\$\begingroup\$ If
BaseException
is conceptually abstract, simply make it mechanically abstract too, look at the edit. Copying ashared_ptr
cannot throw, as it just involves incrementing a reference-count. And dealing withstd::string
s at all won't save an allocation:std::make_shared
generally allocates the shared object, and the reference-counts, in one piece. Of Course it doesn't matter whether the used string-type is "basic", as long as it does copy-on-write, which is forbidden forstd::string
. \$\endgroup\$Deduplicator– Deduplicator2019年05月05日 10:55:45 +00:00Commented May 5, 2019 at 10:55 -
\$\begingroup\$ I meant copying the characters, as in
std::copy_n(&ts[0], ts.size(), p)
. Regarding thestd::string
: suppose there are throw clauses that construct astd::string
as thedetails
message:throw IOError(filename + "!")
(filename
is astd::string
, as in the example above). If such uses were frequent, wouldn't taking the string as aconst std::string &
be better than having to force the thrower to give achar *
(throw IOError(filename.c_str())
) and then copy all of the characters in a newly allocatedchar[]
(throughmake_shared
)? Maybe I missed something about what you said. \$\endgroup\$Anakhand– Anakhand2019年05月05日 19:09:50 +00:00Commented May 5, 2019 at 19:09 -
1\$\begingroup\$ @Anakhand Prefer
std::string_view
overstd::string const&
as a function-argument, wherever you don't need the 0-terminator. You can cheaply construct the view even in many cases where you would have to allocate for a temporary std::string. And the only part in my replacement that can throw is the call tostd::make_shared
, because some dynamic memory is needed. \$\endgroup\$Deduplicator– Deduplicator2019年05月05日 19:25:47 +00:00Commented May 5, 2019 at 19:25
Explore related questions
See similar questions with these tags.