2017-06-06 4:35 GMT+02:00 Gavin Lambert via Boost
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.
But does this preserve the guarantee that result