Andrzej Krzemienski wrote:
So, does the following recommendation correctly capture the design goals for boost::variant2?
If you require the never-empty guarantee (and accept the costs) use boost::variant2.
If you do not require the never empty guarantee use std::variant.
Kind of, but as written this implies that std::variant has no costs, which is not true. The checks for valueless do carry a cost. Each visit(), for example, starts with `if(valueless) throw`, which is not necessary in variant2.
Also, I am not entirely satisfied with the reply, "those who want this guarantee". Could you, or anyone else, give me a real-world use case where a never-empty guarantee is needed, but a strong exception guarantee is not?
My reply was unsatisfactory because I was really not looking forward to rehashing the arguments against singular states. Singular state is bad when a result of two-phase construction (which is why we no longer use two-phase construction), it's bad when a result of exception (which is why we don't use destroy-only exception safety but basic exception safety), it's bad when a result of default initialization of built-in types (but we can do nothing about it), and it's bad when a result of a move (which is why move semantics, as originally specified, do not put the moved-from object in a singular state.) Singular states introduce implicit "is_valid" preconditions on all your normal functions, and partition the program into two worlds, a normal world where no object is singular, and an "exceptional" world where objects may be singular. It's _possible_ to program in this way, but it's not fun, because world #2 may never call into world #1 under penalty of undefined behavior, and singular objects are never to enter world #1, because this sets up a delayed explosion. If you avoid singular states, this removes all these implicit "is_valid" preconditions, which removes the partitioning and collapses the two worlds back into one; "type 2" code can call "type 1" code and nothing undefined will happen. Now in principle, for the specific case of move, it's possible and sound to specify it to leave the object in a singular state, provided that you only ever move from objects that are about to be immediately destroyed. But that's not the approach that was taken. In this timeline, move does not leave objects in a singular state, so there is no requirement to only ever use it on objects that are about to be destroyed. For variant specifically, the guarantee that variant<X, Y> can only ever either hold an X, or hold a Y, simplifies the specification of all code taking variant<X, Y>, because it's not required to document what happens in the event of the variant not holding X or Y. "Never empty" is somewhat a misnomer, because variant2 can be empty, you just have to request it explicitly: variant<monostate, X, Y>. Of course then you have to explicitly handle the possibility of the variant being empty. If the variant is an implementation detail of some component of yours that has behavior X' when in state X and behavior Y' when in state Y, you would need to decide what happens when the variant is empty. Do you emulate X' or Y', or does the component behave in a third way, E'? Up to you, but undefined behavior is probably not acceptable, unless you introduce a singular state for your component. What are the alternatives? One is to do what std::variant does and try to have the cake and eat it too. Have the empty state, but don't acknowledge it in the interface as equal to other states, throw an exception instead. This is a bit like sweeping the problem under the carpet, it allows people to pretend that the variant delivers the "never empty" guarantee and program as if it did, whereas it doesn't. If you were serious about your convictions you would make accessing a valueless variant undefined behavior, which avoids the valueless checks but see above about singular states and time bombs.