
On 30/05/2017 22:18, Peter Dimov via Boost wrote:
Getting people a library that has a multitude of result classes would obviously be better than nothing, but how would it move us toward the goal of having std::result?
How Boost originally worked was, when there is disagreement as to how some component should work, we hammer out our differences here, produce a consensus design, produce, as a result of our process, a rationale of why we arrived at this consensus and how, get the library into the hands of our users, listen to their feedback, refine as necessary, then hand the finished product to the LWG.
When some people want X, others Y, a third group Z, it's always the path of least resistance to just put X+Y+Z into the library, or a policy-based factory that can create 6^11 classes, among them X, Y, and Z. But that's really a cop-out.
You may remember back at the very beginning of this review I said that these objects were different to normal C++ design principles, there was a multitude of ways of using them and hence so many varieties of usage style in their API. I said that for most of my libraries, I decide on one single clean design and use model, but for this library I really did feel that multi-modal usage was more appropriate. Thanks to this review we have reduced down that multi-modal API into simplified categories, and hived those out into separate types so the type tells the user the semantic, all with a substantial reduction in member function count. All a definite improvement in my opinion. I don't agree that this is a cop-out. I believe that empty vs non-empty is a very obvious design improvement, it significantly improves the clarity of contract of public APIs using these objects by indicating whether an empty state can ever be returned. The wide vs narrow is much less obvious. During these last few days to put code to the problem, I put together a toy Outcome implementation using nothing but templates and std::variant<> (my very first ever C++ 17 program yay!) using: // Statically checked T|error_code template <class T> using static_checked_result = outcome<T, std::error_code, void, default_to::disabled, emptiness::never, observers::narrow>; // Runtime checked T|error_code template <class T> using runtime_checked_result = outcome<T, std::error_code, void, default_to::empty, emptiness::formal, observers::wide>; ... and tried using them in various use cases. I can find absolutely no difference in runtime overhead. So that makes the sole difference to be whether you prefer the compiler to tell you about the obvious null pointer dereference in *std::get_if(&variant) if it's clever enough, or whether you prefer some runtime default action to occur instead of dereferencing a null pointer which is an instant segfault. The former traps logic errors missed by the compiler at runtime effectively. The latter lets the programmer skip typing out boilerplate. But flipping the perspective, the former means your program may die unexpectedly in production due to some untested branch combination. The latter hides logic errors and therefore programmer bugs. I know you think this a cop out, but I think those two are unresolvable. Outcome may simply choose to implement one set and not the other out of ideological purity, but I think that a "cut off one's nose to spite one's face" type of decision. It's a different form of cop out: we can't resolve this, so we choose one and screw the other.
(*) Not quite because the straightforward implementation would throw system_error instead of filesystem_error on value() and we'll lose the path, which is an interesting angle that we need to explore.
What we really actually need is an error_code which can carry payload. Lawrence's status_code is heading the right way, but I am not keen on his formulation. I have a gut feeling that we can do better, though avoiding malloc and keeping the object useful is hard. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/