There are several things in tension here, so let me please enumerate them:
- centralization / modularity: it is normal to think about centralizing things, as it allows to easily check for the kind of error one should expect and recover an error from an approximate souvenir etc... however modularity asks that each module be able to introduce its new errors
- error may happen during dynamic initialization (unless you ban code from running then, not easy to check), to circumvent the lifetime issue, it is better to only rely on objects that will be initialized during static initialization (this is the case for string literals, for example)
In general, the simplest thing I have seen was to use some integral constant to identify an error and then have a table on the side in which you could retrieve other information (potentially a lot of it). For example, Clang uses this system for its diagnosis. You can avoid repeating yourself by using the preprocessor at your advantage.
Use such a header:
#define MYMODULE_ERROR_LIST \
ERROR(Code, "description") \
...
#define ERROR(Code, Desc) Code,
class enum ErrorCode: unsigned {
MYMODULE_ERROR_List
NumberOfElements
};
#undef ERROR
struct Error {
ErrorCode code;
char const* description;
};
Error const& error(ErrorCode ec);
And a simple source file to locate the array:
#define ERROR(Code, Desc) { Code, Desc },
Error const ErrorsArray[] = {
MYMODULE_ERROR_LIST
{ErrorCode::NumberOfElements, 0}
};
Error const& error(ErrorCode const ec) {
assert(unsigned(ec) < unsigned(ErrorCode::NumberOfElements) &&
"The error code must have been corrupted.");
return ErrorsArray[ec];
} // error
Note: the price of defining the macro in the header is that the slightest change of wording in a description implies a recompilation of all the code depending on the enum. Personally, my stuff builds much faster than its tested, so I don't care much.
This is quite an efficient scheme. As it respects DRY (Don't Repeat Yourself) it also ensures that the code-description mapping is accurate. The ERROR
macro can be tweaked to have more information than just a description too (category, etc...). As long as the Error
type is_trivially_constructible
, the array can be initialized statically which avoids lifetime issues.
Unfortunately though, the enum
is not so good at modularity; and having each module sporting its own enum
can soon get boring when it comes to dealing uniformly with the errors (the Error
struct could use an unsigned code;
).
More involved mechanisms are required if you wish to get beyond a central repository, but since it seemd to suit you I'll stop at mentioning this limitation.