czw., 18 kwi 2019 o 19:57 Nevin Liber via Boost
On Thu, Apr 18, 2019 at 2:41 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
x1 = std::move(x2);
This throws, and leaves the objects in an unspecified (albeit "valid") state, an exception handling is stopped prematurely, so that the objects remain in scope and the programmer makes no attempt to put their values into a known state. The only guarantee we have now is that no operation on `x1` or `x2` will cause an UB.
Not true. The only operations you can perform on those objects are either (a) ones with no preconditions or (b) those where the preconditions are checked first and found to be valid.
To put more context to this remark, the quoted text is me trying to explore Peter's variant or another type with strong invariant, where the guarantee is fulfilled by inventing values on the fly. I agree with your statement, that in practically any type we will have functions with narrow contract. But I think it misses the core of the disagreement, which I believe to be: can you still do a lot of things with this object or only a very narrow subset of things? The goal of the never-empty basic guarantee is to have less UB's in the code (or less defensive checks).
The problem with an illegal-to-observe zombie state is that it adds an unexpected precondition to copy/move construction, and that precondition is viral. For instance, having such a precondition breaks the following:
void ByRef(vector<PossiblyZombieObject>& v) { v.resize(v.capacity() + 1); // error if v contains any zombie objects }
or even
void ByValue(vector<PossiblyZombieObject> v) { /* ... */ } // error if v contains any zombie objects
And even if callees decided that want to protect against this (and let me reiterate, that should never be a requirement), they cannot, precisely because the zombie state is illegal-to-observe.
A std::variant in the valueless_by_exception state is copy/move constructible because that state is observable, and that is by design. Specifically, valueless_by_exception is not a zombie state, but it is a weakening of the invariants from "exactly 1 of n types" to "at most 1 of n types". How much that matters is ultimately what we are debating.
Thanks for this observation. Maybe my adapting the word "zombie state" was unfortunate as it may imply things that I never wanted to imply. In the model that I would like to propose with "strict invariant" and an "effective invariant" there is basically no problem with copying and moving the "special state". The special state is the difference between the "effective invariant" and "strict invariant" , in case of variant it is .valueless_by_exception(). This state can simply be propagated during move/copy. This preserves the information about the bug. However, any situations were I can imagine this would be needed would be again in the places that one should not have in the program: where you allow the objects that threw from the basic-guarantee operation to survive, or their state to survive. Even your examples above. If you are manipulating a PossiblyZombieObject in a vector and it throws, the object gets "corrupted" and unless you reset it to a known state it corrupts the entire vector, so either you have to reset the vector or destroy it. Resizing at this point is surely a bug. A second thought. I say the special state should never be observed, but it is a bit of a simplification. There is one situation where observing the special state makes sense: it is in the function valueless_by_exception() for variant, and in general case it could be called is_in_weak_state() or some such. If not anything else, it would be used to describe preconditions of other functions, and it could be used in clever ways for assisting static analysis or debugging. However correct programs should never have a need for the users of a given type to call this function to "check" if an object is in special state: in correct programs it is never observed. Regards, &rzej;