This PR is part suggestion, part implementation. If you can think of an easier to do what I'm trying to do, I'm all ears.
I have an application that has a lot of duplication in terms of issuing diagnostics for error handling, and I want to keep the error handling localized as much as possible. It has an intrinsic hierarchy that mirrors the error-generating code, but I can't represent that hierarchy well with just a flat sequence of handlers. Shortened example:
leaf::try_catch(
[] { return open_project (); },
[](e_project_path path, e_missing_file missing) {
log("Error while opening project in {}", path.value);
log("Missing file: {}", missing.value);
},
[](e_project_path path, e_json_string json_str, e_json_parse_error json_err) {
log("Error while opening project in {}", path.value);
log("Error in JSON '{}'", json_str.value);
log("Invalid JSON: {}", json_err.value);
},
[](e_project_path path, e_json_string json_str, e_json_schema_error err) {
log("Error while opening project in {}", path.value);
log("Error in JSON '{}'", json_str.value);
log("JSON data is invalid: {}", err.value);
});
In reality there can be dozens of handlers that have some amount of duplication between them, and it can become difficult to browse and update as error conditions are added.
This PR adds leaf::handle_more
that can be used to do nested error handling. It looks like this:
leaf::try_catch(
[] { return open_project (); },
[](e_project_path path) {
log("Error while opening project in {}", path.value);
return leaf::handle_more(
[](e_missing_file missing) {
log("Missing file: {}", missing.value);
},
[](e_json_string json_str) {
log("Error in JSON '{}'", json_str.value);
return leaf::handle_more(
[](e_json_parse_error err)
{ log("Invalid JSON: {}", err.value); },
[](e_json_schema_error err)
{ log("JSON data is invalid: {}", err.value); });
});
});
Here are the semantics:
- The return type of
handle_more
is an opaque type that captures the handler types and a "return type" which can be provided explicitly as the sole template argument or will be deduced as the common type of the return types of the handlers (If another nested handler returns another handle_more
, the common type will "unwrap" the handle_more
-return-type for that handler).
- The
context
deduction helpers recognize if a handler returns handle_more
, and will recurse into its nested handlers to find additional types that it needs to create slots for.
- The nested handler is invoked directly within the
handle_more
evaluation.
- The return values from
handle_more
will propagate out: So the handle_more
handlers need to have a return value that is convertible to the overall return type of the try_handle_{some,all}
.
Here's the implementation details:
- The return type of
handle_more
is leaf_detail::more_handlers_result<R, H...>
, where R
is the common result type and H...
are the inner handlers.
- The machinery of
handle_more
is in handler_caller
, which will recognize a handler that returns a more_handlers_result
.
- The best way I can initially think to get the slots tuple down into
handle_more
was by using a thread-local pointer to an abstract class with a virtual R invoke(H&&...)
(more_handler_invoker_base<R, H...>
). The handler_caller
creates a concrete instance that stores the slots and error_info
, and then stores the thread-local pointer-to-base for that invoker. handle_more
then loads that pointer and calls invoke(hs...)
, which then does the actual handle_error_(tup, ei, hs...)
. This trickery with thread-local pointers feels a bit hairy, though...?
- The
handler_caller
will return the actual return value of the handler by unwrapping it from the more_handlers_result
object.
Open issues with the current implementation:
- In the above example, the handler for
e_project_path
will be invoked if that slot is loaded, but it is possible that none of the nested handlers would be satisfied. Currently handle_more
requires an "everything" handler at the end (like with try_handle_all
), but it would be better if it recognized whether it was being called for try_handle_all
or try_handle_some
before requiring a catch-all handler or not.
- The thread-local pointer is loaded before the handler containing the
handle_more
is invoked, and only loaded when handle_more
is invoked later on. In a pathological case, some code in between those two points could change the pointer and this will explode in handle_more
:slightly_smiling_face:.