Boost logo

Boost :

Subject: Re: [boost] [variant2] Need rationale for never-empty guarantee
From: Niall Douglas (s_sourceforge_at_[hidden])
Date: 2019-03-02 18:25:29


>> Thus I would hold that, unless there is a *very* good reason why not, so
>> should std::variant propagate the strong guarantee where it is able to
>> do so. To my knowledge, there is no good reason that is does not, as
>> proven by Peter's variant2.
>
> Hold on. Most standard library types *do not* provide strong exception
> safety guarantee on assignment. It is only the subset of STL containers
> that are implemented as pointers, such as std::vector or std::list.

You see, I would consider any union-based storage as in the same
category. By definition union-based storage must contain a "pointer" to
the correct way to interpret that union-based storage.

>
 But
> consider std::array, which has to store its elements directly. It has no
> way of providing strong guarantee on assignment. Or consider std::pair:
>
> pair<string, string> p1 {"Niall", "Douglas"};
> pair<string, string> p2 {"Peter", "Dimov"};
> try { p1 = p2; } catch(...) {}
>
> If the assignment of p2.second() throws, you end up with unintended person
> {"Peter", "Douglas"}. The same with nearly every aggregate you might use in
> the program:

And which is the same in Outcome. Aggregate storage has the property
that aborting moves mid-stride leaves the aggregate partially moved-to
or moved-from. Everybody expects that, which is why it is important to
preserve that behaviour (and Outcome does).

But union-based storage does NOT have that property. There is any one of
<T ...>. I expect T...'s triviality, noexcept, and all other guarantees
to be propagated where possible. Unless there is a very good reason not to.

> struct Person
> {
> string firstName, lastName;
> };
>
> The generated assignment only provides *basic* exception safety guarantee.

I wouldn't look at it that way. I would say that an aggregate is like an
array. Each type in the array/aggregate provides its own guarantees. The
aggregate does not interfere with each type's guarantees. Each member is
"atomic" in this regard.

> Most of the types we use only provide basic exception safety guarantee in
> assignment. But it is usually not a problem, because in a program that
> correctly handles exceptions all objects that cause exceptions are
> immediately destroyed in stack unwinding.

Global state is left in an intermediate state. At the "high water mark"
where the exception throw occurred.

I get your point that global state ought to be explicitly unwound. But
there are idempotent global state designs where interruption at any
point does not matter. A trap-state variant just doesn't fit into such
designs, so I can't use std::variant. That annoys me.

> variant2 also only provides *basic* exception safety guarantee: you can be
> assigning a variant containing type B to a variant containing type A and
> end up with variant containing type C. Here's an example:
> https://wandbox.org/permlink/AObFiUKgeXIEiXQa

I just don't get why variant2 would set a state of C when it had state
A, and setting state B failed. It should have spotted the lack of common
noexcept move, employed double buffers, and alternated between them such
that state A is untouched should setting state B fail.

If it's going to do weirdness like setting state C out of the blue, then
better dispose entirely any double buffered implementation as not adding
value.

Niall


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