Boost logo

Boost :

Subject: Re: [boost] [outcome] Exception safety guarantees
From: Vicente J. Botet Escriba (vicente.botet_at_[hidden])
Date: 2017-05-28 09:12:17


Le 27/05/2017 à 18:35, Andrzej Krzemienski via Boost a écrit :
> Hi All,
>
> This is in connection with Vicente's question: what exception safety should
> we expect of copy assignment of `expected`?
>
> Now, this is really funny because we are talking about exception safety in
> something that is used as a substitute for exceptions.
This is not the intent of std::experimental::expected. Otherwise I will
have not added any function that throws.
Expected is for the expected errors. Exceptions is for the Exceptional
errors. Both could work together.
> That is, one of the
> use cases for `expected` is to be able to get rid of exception handling
> altogether (while still reporting failures). But this fundamental problem,
> "what state my object is in if this mutating operation fails" is not
> specific to exceptions. It is speciffic to operations that might fail; and
> how (and if) they signal failures is of secondary importance. So, let's
> talk about *failure safety* rather than 'exception safety'.
Currently we are unable to signal failure on constructors and assignment
without exceptions.
>
> First, let's consider the use case for `expected`, where we want to disable
> exception handling altogether. This means `T` or `E` cannot throw on any
> operation. But copy assignment can still fail, right? Or maybe in these
> domains you are only using trivially-copyable types as `T` and `E`. Or
> maybe in these domains you never have a need to copy-assign instances of
> `expected`? Is so, they provide a *no-fail* guarantee, and any
> implementation of `expected` will be good and offer no-fail guarantee also.
> Bus if some copying operation on T or E can fail, how is the failure
> reported? Through a return value? Output parameter?
Exceptions :)

If you want to report errors from constructors without exceptions you
could replace constructors by factories that return expected.
If you want to report errors from assignment without exceptions you
could assignment by a different operation that returns expected to
transport the error.

The proposed Expected has not considered the last. It will be
interesting to see the impact of this kind of assignments.
>
> But whatever the answer, we are arriving at the "nested failure" problem:
> we are processing a (potential) failure report, and this processing fails.
> What should we do? report the new error condition and ignore the previous?
> This is very close to a double-exception during stack unwinding. In C++ it
> std::terminates, other languages ignore the original error, or build a
> combined error report. All these solutions not satisfactory, and maybe no
> satisfactory solution exists.
This is why expected need to constraint the Error type.
What happens if you want to throw an std::string exception and there is
an exception while constructing it?
>
> I would like to hear an opinion from people who deal with `expected` in
> exception-disabled environments.
I'm out of this perimeter.
>
> On the other extreme, you fave my example with parsing input (which Vicente
> observed is not parsing, but matching):
> https://github.com/akrzemi1/__sandbox__/blob/master/outcome_practical_example.md
>
> In that case, If I get an exception anywhere (not only upon copying T or E)
> I want stack to be unwound so far, that all not `expected` objects will
> remain. So I only care about basic failure guarantee: just let me correctly
> destroy these objects.
>
> In the middle: you have the situation where you copy-assign an `expected`
> and a copy-constructor of assignment of T or E throws. But where did this
> exception come from, given that you are using `excepted` for signalling
> failures? Or are you signalling some failures with exceptions and some with
> `expected`? And if so, are exceptions not more panic-like? And in that case
> yu would like to abandon the processing of any `expected`?
You may be right, that is some cases the state of the original expected
has no importance (when it was a local variable, but in other cases it
has. Think of an expected stored somewhere. You function is returning
this expected but this doesn't mean that other part of the code cannot
inspect this stored expected later on.
You have failed returning this expected. Why do you want to modify the
stored one?
>
> Anyway, the most difficulties stem from the case where you are storing an E
> in `expected` and you want to assign an `expected` storing T. You have to
> first destroy E, and then may not be able to construct a T.
See Anthony paper.

When you are assigning a T to an expected you are not reporting an
error, so I don't see the problem.
If the assignment fails because T throws, the original expected is not
changed and the exception is just thrown.

In a world where construction and assignment has no error expected is
much easier to implement.
>
> My solution to this would be to go to the advice from the first days of
> forming exception safety guarantees: provide basic guarantee by default,
> and strong guarantee only if it does not cost too much. We are used to STL
> containers providing strong assignment, but this is because they are
> pointers, and they can implement it for free. But does std::touple provide
> a strong guarantee? No. Do aggregate types provide stron guarantee? No. And
> can it result in inconsistent data? It can:
>
> ```
> struct Man { std::string fist_name, last_name; };
> Man m1 = {"April", "Jones"};
> Man m2 = {"Theresa", "May"};
>
> try {
> m2 = m1;
> }
> catch(...) {
> }
> ```
>
> `m2` may end up being {"April", "May"}.
Right. This is how it is defined now.
> And we are taught to write types
> like this. But if this hapens, the blame is on whoever allowed these
> objects to outlive the "stack unwinding bubble".
>
> So my view, as of today, is not to strive for a strong or even never-empty
> guarantee. Provide a conditional guarantee, if types T and E don't throw,
> you get no-fail guarantee. If they do, you only have a basic guarantee: you
> can destroy, assign to, or maybe call valueless_by_exception(). Nothing
> more. I think `std::variant` made the optimal choice.
Andrzej, please, could you try to replace your parser example with
std::variant<T,E> and comeback with your experience.

>
> And people should code so that instances of `expected` (at least those with
> T or E throwing on copy/move) should not outlive the "stack unwinding
> bubbles".

For this we should be able to ensure that expected is used only on the
stack, and I believe we don't know how to constrain a type to live only
on the stack. In addition, this is not always the case. We want to store
expected outside the stack.

Vicente


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