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
p1 {"Niall", "Douglas"}; pair 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
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