On 5/06/2017 11:08, Peter Dimov wrote:
The correct approach here, I think, is not to define fat error
codes, but to return an outcome whose error() is the error_code and
whose exception() is the appropriate filesystem_error exception,
complete with the path(s).
This sounds like the design I was suggesting in the "to variant or not
to variant" thread.
On 6/06/2017 02:46, Peter Dimov wrote:
Vicente J. Botet Escriba wrote:
Oh, in this case it is clear that result<T> is not enough. We need
expected
where filesystem:error contains the error_code and the path.
I don't find this particularly appealing, because the whole point of
std::error_code was to avoid the need for each library to define
separate error classes. expected will work, I'd
just not be willing to recommend it as a general solution.
Is it more attractive if perhaps we had a result where E was
constrained to a type derived from std::error_code? (I'm imagining a
sort of orthogonal hierarchy with types derived from std::exception,
although not for error-identification purposes, just to carry additional
data payloads.) Implicit slicing would allow this to be used
generically in contexts where an error_code is expected.
The error_code design is a little unfortunate since users are encouraged
to pass them around by value, which would slice off additional context
information. OTOH even if passed around by reference so the information
is preserved, it wouldn't be readily accessible without dynamic_casts.
(The same is true for exceptions as well, but some language/compiler
conspiracy hides this.)
The idea of error_code having an arbitrary error-info pointer seems
attractive, but the above semantics would probably require it being a
shared_ptr<void>, which would probably annoy everybody for various
reasons (including type safety, atomics, and memory allocation).
Niall's error_code_extended is a sort of half-way point where it
provides some additional fixed data payload which is probably useful in
many cases as a compromise type; additionally defined as POD to avoid
dynamic allocation and so that the ring buffer stomping over the data
doesn't upset too many things. It's a good idea, and I'm not sure if we
can come up with something better (other than arguing about what members
it should have), but perhaps there are some other possibilities to consider.
Perhaps rather than allowing arbitrary Es we could have a single E that
still provides some additional flexibility:
template<typename EI>
struct error_code_info : public error_code
{
// ...
const optional<EI>& error_info() const;
// ...
optional<EI> m_error_info;
};
template
using result = expected_impl;
(Again, just a sketch to present an idea; don't get too hung up on the
specifics.)
Could something like this work? An error can be specified with an
arbitrary data payload, and yet consumers could just slice that off and
treat it as a plain error_code if they want. Meanwhile the
result/expected implementation can rely on noexcept/move guarantees
provided by error_code_info.
The main complication I see with this is that you probably don't want to
over-constrain the EI type, to simplify usage -- that's why I suggested
it store optional<EI> rather than EI directly, so that error_code_info
could be noexcept in all cases; if EI doesn't have noexcept move/copy
then error_code_info just discards the info if EI's constructor throws.
(So this does allow an empty state and the empty state should not be
considered weird; consumer code just needs to tolerate that like they
would any other optional<>.)
This does mean that consumers can introduce memory allocation into their
error codes (probably via std::string), which some people won't like --
but this leaves the choice of doing that or avoiding that up to them
(and the libraries they choose to consume).
The main downside I see of this is that it's less straightforward to
pass around error_code_info<EI>s than error_codes, but since this should
be mostly confined to a well defined call chain (with specific concrete
EI values rather than generics) I hope this wouldn't be a problem in
actual practice. (Using a shared_ptr<void> would avoid that issue but I
think that's probably worse overall.)
It might also result in a proliferation of EI types (as each method of
each library could conceivably define a unique one) but again I doubt
that should be an issue in practice.