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.

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

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


Boost list run by bdawes at, gregod at, cpdaniel at, john at