I don’t think these are really independent. Whether or not you need to emit
a diagnostic depends on whether a caller can handle the corresponding error,
which isn’t something you know at the point where the error is raised.
But you do in the diag handler. For example, if you are trying to open
multiple files, some of which are bitcode, you know to ignore
InvalidBitcodeSignature.
That means anticipating the kinds of errors you do/don’t want to recover from at the point at which you insatiate your diagnostic handler. At that point you may not have the information that you need to know whether you want to recover. E.g. What if one path through library code can recover, and another can’t? Worse still, what if the divergence point between these two paths is in library code itself? Now you need to thread two diagnostic handlers up to that point. This scales very poorly.
Simply put: diagnostic handlers aren’t library friendly. It’s tough enough to get all libraries to agree on an error type, without having them all agree on a diagnostic handlers too, and thread them everywhere.
This highlights why I think it is important to keep diagnostics and
errors separate. In the solution you have there is a need to allocate
a std::string, even if that is never used.
Errors are only constructed on the error path. There is no construction on the success path.
If it was always a
std::string it would not be that bad, but the framework seems designed
to allow even more context to be stored, opening the way for fun cases
of error handling interfering with lifetime management.
This is a real problem, but I think the solution is to have a style guideline: You can’t use reference types (in the general sense - references, pointers, etc.) in errors. Errors that would otherwise require references should have their error-message stringified at the point of creation.
Consider someone who wants to use libObject to write an object-file repair
tool: A “bad_symbol” error code tells you what went wrong, but not where or
why, so it’s not much use in attempting a fix. On the other hand “bad_symbol
{ idx = 10 }” gives you enough information to pop up a dialog, ask the user
what the symbol at index 10 should be, then re-write that symbol table
entry.
Using error results to find deep information you want to patch seems
like a bad idea. A tool that wants to patch symbol indexes should be
reading them directly.
I’m not sure I agree, but I don’t have strong feelings on this particular example - it was intended as a straw man. My point is that error recover is useful in general: there is a reason things like exceptions exist. Diagnostic handlers are very poorly suited to general error recovery.
void check(TypedError Err) {
if (!Err)
return;
logAllUnhandledErrors(std::move(Err), errs(), “”);
exit(1);
}
That is not the same that you get with a diagnostic handler. What you
get is an exit after the error was propagated over the library layer,
instead of an exit at the point where the issue is found.
As long as you log the error I’m not sure I see a meaningful difference? If you’re not debugging you shouldn’t be crashing in your library. If you are debugging you want to set a breakpoint on the error constructor instead.
I would prefer the diagnostic handler for exactly the same reason 
Generality has a cost…
Could you explain what you see as the cost in this instance?
The scheme has a higher cost that std::error_code alone when you’re returning an error, but I don’t think optimizing the error case is a sensible thing to do. It has a potentially lower cost on the success path, as you don’t need to take up an argument for the diagnostic handler.
The most serious objection that you’ve raised, to my mind, is the potential lifetime issues if people mistakenly use references in their error types. I’m planning to write additions to llvm’s coding guidelines to cover that, which I think should be sufficient.
On the flip side, compared to a diagnostic handler, I think the scheme I’ve proposed has the following benefits:
- No need to thread a diagnostic handler type through the world (and consequently no need to agree on its type). Retrofitting rich error logging to an interface that already returns errors requires less work than adding a diagnostic handler.
- The ability to describe error hierarchies (e.g. the class of all errors that represent malformed objects, which may be further subclassed to describe specific errors).
- The ability for client code to meaningfully distinguish between error instances, rather than just error types.
, and I don’t think we should invest on it until
we have a concrete case where that is needed.
The error infrastructure investment is largely done, and I’m volunteering to maintain it. Myself and Kevin Enderby are signing up to weave this through libObject and the JIT. Other libraries could be converted as people see fit, with ECError providing an interface between the std::error_code world and TypedError.
Given that I would
suggest going the more incremental way. Add a CheckedError that wraps
error_code and use to make sure the errors are checked. When a better
diagnostic is needed, pass in a diagnostic handler.
Our first use-case for this system is libObject, where we want to use this to enable richer error messages. An incremental approach would involve threading a diagnostic handler through everything (and returning CheckedError), then un-threading it again would involve a lot of churn. I’d prefer to move directly to the scheme I’m proposing.
Cheers,
Lang.