Boost logo

Boost :

Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2018-02-03 13:56:35


2018-02-02 23:26 GMT+01:00 Emil Dotchevski via Boost <boost_at_[hidden]>
:

> Consider the following library:
>
> template <class T,class E> class result; //contains variant<T,E>
>
> conversion to bool returns true if result was initialized with T, false
> otherwise;
>
> .value() returns T& or calls boost::throw_exception(error());
>
> .error() returns E& or undefined behavor.
>
> You could use this library together with exception handling: it helps with
> annoying exceptions when you can handle the error locally, otherwise you
> just call .value() and let exceptions do their thing.
>
> The question is what to do under BOOST_NO_EXCEPTIONS. Presumably you could
> go with E=error_code, but that is inadequate for forwarding arbitrary
> errors, making the library impractical to use without exceptions -- and we
> want a library that is practical to use without exceptions. Correct me if
> I'm wrong, but I think that you and I agree on this.
>

I must admit I never had to write a program with exceptions disabled; but I
assume that when you make this decision, you also sketch out the strategy
for dealing with function failures. Maybe you just std::terminate on
resource shortage, maybe something else; but I am pretty sure you cannot
just say "disable exceptions, and we will deal with failures similarly".
This requires the alternate design of the entire program that indicates how
failures are communicated and propagated. Technically, this is possible
with errno or returning int values representing errro conditions tangled
with many if-statements. But that is really error-prone. Using Outcome here
is definitely superior to errno and manual if-statements. You say that it
is inferior to exceptions, but hey, we are compiling with exceptions
disabled. We will accept some compromises in this case.

Also, the programs where you disable exceptions would be doing low-level
stuff. And there usually you would report failures through integers. I
think this is the acceptable compromise when not using exceptions. Once you
can accept this cost std::error_code gives you a convenient tool: two ints,
one to identify the library, the other to identify the condition in this
library. Once you accept this cost. The failure condition recorded in
std::error_code never changes, it never needs to be translated to anything
else. std::error code addresses your concern about potential translations
of exceptions. It does not address your concern about communicating
arbitrary data. But in the end it might turn out to be a reasonable
trade-off for low-level libraries that disable exceptions.

This is one of the applications of boost.Outcome. It is superior to naked
std::error_code because it can additionally guarantee that no failure is
accidentally ignored by the programmer. I think that Boost.Outcome could be
used in tandem with Boost.Noexcept, to use the latter's TLS.

> 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?
>

I don't know. Maybe I pay too little attention to this social aspect. I
sympathize with this view. Some people do not understand the philosophy,
solutions, and performance implications of parts of C++ and rather than
trying to understand them, they try to pretend that C++ is a different
language. I admit I sometimes wish we didn't have `shared_ptr`. Do not get
me wrong. I realize it is useful, and it is the best tool for doing certain
things. But in all the projects I worked for, I have far more often seen it
abused or overused, than I have seen used it properly. But if I ask myself
if Boost and STD would be better without shared_ptr, I would not be able to
say "yes". Some people will overuse it, but others will benefit from using
it correctly. Maybe what could have been done better in Outcome is to lay
this out clearer in the introduction. That the goal is not to turn C++ into
Rust.

>
> 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-
> exception-specifications/
> .)
>
> 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.
>
> 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 to the caller.
>
> (I see these as mutually-exclusive).
>

I can see the point you are making. The analogy to exception specifications
is a bit distracting, because I think there was more than one problem with
it. The goals you describe are mutually exclusive in the most general case.
However it looks like Outcome provides a solution for most of the practical
cases, while leaving the general case unsolved. Boost.Outcome is a set of
tools (rather than just one) and you are expected to choose one that best
solves your particular problem.

Remember that the goals you list and arguments that Herb Sutter draws apply
to a general failure object transportation mechanism present everywhere in
the program, in any program. In contrast, Outcome is not intended to be a
failure reporting mechanism in the entire program: it is either to be used
in isolated places (with particular conditions), or in programs with
extremely harsh execution constraints, where many inconveniences are
expected, including the inability to freely transport arbitrary amount of
failure information in arbitrary form.

So, case 1. You are using in your program a boost::filesystem2 library ("2"
because we hypothetically assume it uses Boost.Outcome to report failures).
Your program can freely use exceptions. But often the inability to write to
the file is not something you have to propagate up, but you know how to
handle it locally. This is not a generic context. I exactly know what
library I am using, and one level up there will be no `result<>`, there
will only be exceptions. In this case the most proper tool from Outcome
toolbox is to create your own type representing the failure code and two
file names, and the usage of `result<>` with your type: `result<T,
FilesystemFailureType>`. The question "how this interacts with `result<T,
SomeOtherType>`" is irrelevant, because there will never be such
interaction.

Case 2. We are dealing with the number of low-level libraries, system
libraries, which are so low-level that they cannot decide whether their
failure will also be treated as a failure in your program. This is a
generic context. Luckily it is acceptable by you that std::error_code is
sufficient to carry the information about the failure. You can accept that
there will be no other context available in the failure objects. In this
case, you would use `result<T>` (second argument defaulted). This can
handle arbitrary number of libraries.

Case 3. In some higher level program you are using a number of low level
libraries which report failures through `result<T>` (Case 2) but
additionally other low level libraries throw exceptions. But you are
writing a task processing framework, and in this framework using exceptions
would be clumsy, because all of the time you have to make decisions if this
condition affects task processing or not. In such case you go with
outcome<T>, which can store both `std::error_code` and `std::exception_ptr`.

There are probably more cases, but my point is:

1. Outcome is not meant to be a full failure-handling framework for every
part of every program (even though it is technically possible to use it
this way).
2. It addresses the specific cases through specific trade-offs where it
does not have to address all the issues of the full failure handling
framework.

Regards,
&rzej;


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk