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_exampl...
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
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