Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
From: Emil Dotchevski (emildotchevski_at_[hidden])
Date: 2018-02-03 02:27:04
On Fri, Feb 2, 2018 at 4:20 PM, Niall Douglas via Boost <
> > So, my vote is based on (in no particular order):
> > 1) In these discussions, I can clearly recognize the dislike for
> > exception handling and even C++ (I don't mean by you personally) that I
> > have been exposed to in the past, since for years I've been surrounded
> > by people who falsely believed that they can't afford exceptions or
> > smart pointers or proper serialization, and they have strong, if
> > incorrect, opinions on what's wrong with C++. I believe that this
> > attitude does not belong to Boost. It's possible that I got this wrong.
> > It may be interesting to know how many of the current users of
> > "standalone" Outcome use Boost in "low latency" environments or at all.
> > Do you have an idea?
> No, nor do I think it matters.
If a significant subset of the users of the library won't touch Boost, I
think it does matter. The reason for this speculation is that I've seen
this before, I know my people. :)
> My secondary use case is that rather than encode the logic for how to
> type erase/rethrow/convert the failure into an exception throw via a
> preprocessor macro which is how it's usually done e.g.
> BOOST_THROW_EXCEPTION(), I'd like to set rules via the type system for
> what is to happen. No macros needed. The policies are very useful for
> this use case.
The macro only captures __FILE__ and __LINE__. The meat is in
boost::throw_exception. If that was customizeable, it would render
exceptions useless in Boost libraries, which are encouraged to use
boost::throw_exception to throw. Let me know if you need me to elaborate
why these are the correct semantics for boost::throw_exception:
> > 2) Clearly, Outcome _does_ want to help pass errors across API
> > boundaries, including in generic contexts. The problem is that
> > result<T,E> compute() noexcept;
> > is very similar to
> > T compute() throw(E);
> > (yes, I know exception specifications are enforced dynamically, but
> > that's not what's wrong with them, see the second question here:
> > https://herbsutter.com/2007/01/24/questions-about-
> > My reasoning is that if with Outcome you can always return the exact
> > error type you've specified in your static interface, the same approach
> > would work for (perhaps statically-enforced) exception specifications.
> The exception specification analogy doesn't apply to Outcome. What
> doomed exception specifications is indirect function calls combined with
> the side channel exception throws operate through
Granted, exceptions are hidden from view, so to speak, but that was exactly
the motivation behind exception specifications, to make what functions may
throw an official, "visible" part of their static interface, so the caller
can reason based on that, just like in Outcome.
> , so your function
> which guarantees to never throw anything but E happens to call some
> overriden virtual function which throws a different type, and boom
> you've just called std::terminate.
Virtual overrides must match the exception specification, but yes, in C++
exception specifications are enforced dynamically.
> That's because exception throws operate via a side channel outside the
> normal flow of execution. Outcome doesn't have that - it returns via the
> normal flow of execution. Therefore we can hard guarantee that if the
> program compiles, the "exception specification" is met.
Agreed, the Outcome "exception specification" is statically enforced. The
Herb Sutter page I linked explains that statically-enforced exception
specifications are better than dynamically-enforced ones.
The thing is (in terms of Outcome), if I've got an error of type E1 which I
can't handle, there is nothing to gain if you force me to convert that to a
type E2, which I also can't handle. Not only this conversion is not always
possible (think generic contexts), but the fact that the error was of type
E1 was lost in translation.
And what is the caller to do with the false E2 I returned if he can't
handle the error either? Translate it to E3, so by the time the error
reaches someone who has to deal with it, he is completely clueless about
what the error was to begin with?
No, if you can't deal with the error at this level, you leave it alone but
help as much as you can. This means adding context-specific information.
Maybe you got a file read error from a low level function, and you happen
to know the file name that you were reading, so you attach that to the
error. And if the error you got wasn't a file read error but a parse error,
what do you do? Attach the file name. What if it is an out of memory error?
Same thing, attach the file name.
> Logically, to address this concern you could:
> > - Demonstrate that there is a major flaw in my analogy, or
> > - demonstrate that exception specifications could be made practical,
> > including in generic contexts, possibly by using some clever
> > policy-based design, or
> > - provide an interface that can forward arbitrary errors ot the caller.
> > (I see these as mutually-exclusive).
> As I pointed out to you during Outcome v1 review, you can implement TLS
> push and pop for Outcome just the same as your Noexcept library does.
This is true, but it doesn't make the complex policy-based translation
machinery any more practical. The problem is the same as in
statically-enforced exception specifications.
I don't think that TLS is the only viable implementation. For example, it's
possible to erase the error type through shared_ptr that uses a custom
allocator to avoid dynamic memory allocation. I have not explored that
design, but I think it has legs.
> As an example, in AFIO, all errored results are recorded into a TLS
> ringbuffer which tracks the execution log, so for any given error, AFIO
> can tell you the exact sequence of API calls, including to internal
> functions, and their parameters, leading up to that point.
The TLS stack, complete with the ability to transport result<T> objects
between threads, as implemented in Noexcept, is not trivial. It's the kind
of tricky bit of code that library developers sweat over so others don't
> It also can
> tell you exactly how AFIO handles the failure, every single function
> called, including internal ones, as the stack is unwound. I haven't
> implemented it yet, but it'll all designed to get fired into a file so
> it acts as an i/o validation audit log
Yes logging is much more important if you don't use exceptions (and I don't
mean this as a negative, sometimes the right thing to do is to not use
exceptions). I think that the reason is that you can't enforce
postconditions, so it is more likely to execute code which shouldn't
execute, and the log is needed to help you figure out which curious parts
of the program control reached this time around.