pt., 1 mar 2019 o 10:59 Peter Dimov via Boost
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
can only ever either hold an X, or hold a Y, simplifies the specification of all code taking variant , 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
. 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.
Thanks for a very long reply. I am sorry if my questions look like trying to repeat the same discussion again. In fact my goal is only to *understand* your rationale behind your design choices; not to argue with them. In your reply you mostly address the issue of a "singular state" in general, but I was hoping for an answer specific to variant. I think the two are different. A general "singular state" is dangerous (I whole-heartedly agree with you here) because users can create this state by using a default constructor or moving from the object. But this is not the case for variant: both default-constructed state and the moved-from state is not "valueless", even in std::variant. The only way to get to a valueless state is to trigger an exception from a move constructor. And it seems to me that when this happens, the only reasonable choice for the user is to either reset or destroy the variant. Therefore the practical implication of having such "variant-speciffic singular state" seems to me not noticeable. 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.
Yes: it makes a lot of sense to me to make an attempt to access the value of a valueless variant an undefined behavior. I do not associate this decision with the problems of types with singular states in general, because there is no easy way to obtain the valueless state in variant; and it seems to me that if we get an exception that puts the variant into this state, and the user makes an effort to intercept the exception and do things with the variant, the user must surely be doing something wrong. So, let me restate my question in more precise terms. Is there a real-world use case for a variant (as opposed to any other UDT), given that default-constructed and moved-from variant is not valueless, where it makes the difference if the variant is guaranteed not to be valueless but does not provide a strong exception safety guarantee? Regards, Andrzej