
Reading boost::variant's documentation: "If any bounded type is nothrow default-constructible (as indicated by boost::has_nothrow_constructor), the library guarantees variant will use only single storage and in-place construction for every bounded type in the variant. Note, however, that in the event of assignment failure, an unspecified nothrow default-constructible bounded type will be default-constructed in the left-hand side operand so as to preserve the never-empty guarantee." Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type? This makes it very difficult for the user to enforce some useful invariants for types that have boost::variant members, which in my mind is very important. Wouldn't it be better to guarantee that operator= either succeeds or doesn't change the observable state of the left-hand side operand, even at the cost of memory allocation? In that case the other behavior may still be provided through a separate function. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

Hi Emil, On Wed, Jul 30, 2008 at 6:36 PM, Emil Dotchevski <emil@revergestudios.com>wrote:
Reading boost::variant's documentation:
"If any bounded type is nothrow default-constructible (as indicated by boost::has_nothrow_constructor), the library guarantees variant will use only single storage and in-place construction for every bounded type in the variant. Note, however, that in the event of assignment failure, an unspecified nothrow default-constructible bounded type will be default-constructed in the left-hand side operand so as to preserve the never-empty guarantee."
Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type?
If an exception is thrown during assignment to a boost::variant<T0, T1, ...>, and one of the Ti is nothrow default-constructible, then the boost::variant will end up with a default-constructed value of one of those Ti. So even if the type happens already to be that Ti, the value will still change to the default value. This makes it very difficult for the user to enforce some useful
invariants for types that have boost::variant members, which in my mind is very important.
I'd like to understand your situation better-- could you elaborate on the use case you are facing?
Wouldn't it be better to guarantee that operator= either succeeds or doesn't change the observable state of the left-hand side operand, even at the cost of memory allocation? In that case the other behavior may still be provided through a separate function.
That would be the strong guarantee of exception safety. Right now, boost::variant provides only the basic guarantee for assignment. As a rule, I think providing the strong guarantee should be done only when it can be implemented efficiently. But again, I'd be interested to know the sort of situation you're facing. Eric

On Sun, Aug 3, 2008 at 8:15 PM, Eric Friedman <ebf@users.sourceforge.net> wrote:
Hi Emil,
On Wed, Jul 30, 2008 at 6:36 PM, Emil Dotchevski <emil@revergestudios.com>wrote:
Reading boost::variant's documentation:
"If any bounded type is nothrow default-constructible (as indicated by boost::has_nothrow_constructor), the library guarantees variant will use only single storage and in-place construction for every bounded type in the variant. Note, however, that in the event of assignment failure, an unspecified nothrow default-constructible bounded type will be default-constructed in the left-hand side operand so as to preserve the never-empty guarantee."
Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type?
If an exception is thrown during assignment to a boost::variant<T0, T1, ...>, and one of the Ti is nothrow default-constructible, then the boost::variant will end up with a default-constructed value of one of those Ti. So even if the type happens already to be that Ti, the value will still change to the default value.
This makes it very difficult for the user to enforce some useful
invariants for types that have boost::variant members, which in my mind is very important.
I'd like to understand your situation better-- could you elaborate on the use case you are facing?
I have 3 variants of values, let's call them V0, V1 and V2. V0 values are of type std::string, and V1 and V2 values are of type float. However, boost::variant<std::string,float,float> is illegal; therefore I'm using: struct value_type { int variant; boost::variant<std::string,float> value; }; For value_type, I want to maintain the invariant that if variant==0, then value is of type std::string, and if variant is either 1 or 2, then value is of type float. The problem is that if std::string operator= throws, boost::variant may break value_type's invariant. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

Emil Dotchevski wrote:
Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type? If an exception is thrown during assignment to a boost::variant<T0, T1, ...>, and one of the Ti is nothrow default-constructible, then the boost::variant will end up with a default-constructed value of one of those Ti. So even if the type happens already to be that Ti, the value will still change to the default value.
I have 3 variants of values, let's call them V0, V1 and V2. V0 values are of type std::string, and V1 and V2 values are of type float. However, boost::variant<std::string,float,float> is illegal;
Then you should use tags, and not ugly hacks. Variant can be implemented in three (four?) different techniques (temporary heap backup, nothrow copy or move, nothrow default constructor). Only the last one (which is, unfortunately, the one invoked by your code), does not satisfy strong exception-safety. It's fairly funny how an optimization diminishes the safety of variant.

On 08/04/08 09:00, Mathias Gaunard wrote:
Emil Dotchevski wrote:
Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type? If an exception is thrown during assignment to a boost::variant<T0, T1, ...>, and one of the Ti is nothrow default-constructible, then the [snip] Then you should use tags, and not ugly hacks. Tags? Do you mean something like:
template<unsigned Tag, typename Type> struct tagged_type { Type a_t; }; boost::variant < tagged_type<0,T0> , tagged_type<1,T1> ...
? In this way, even if T0==T1==...==Tn, the variant would still work but I guess the tagged_type CTOR would have to forward its CTOR args to the a_t.

On Mon, Aug 4, 2008 at 7:00 AM, Mathias Gaunard <mathias.gaunard@ens-lyon.org> wrote:
Emil Dotchevski wrote:
Do I read correctly that in an attempt to assign one variant object to another, the left-hand object _might_ silently change its type? If an exception is thrown during assignment to a boost::variant<T0, T1, ...>, and one of the Ti is nothrow default-constructible, then the boost::variant will end up with a default-constructed value of one of those Ti. So even if the type happens already to be that Ti, the value will still change to the default value.
I have 3 variants of values, let's call them V0, V1 and V2. V0 values are of type std::string, and V1 and V2 values are of type float. However, boost::variant<std::string,float,float> is illegal;
Then you should use tags, and not ugly hacks.
Variant can be implemented in three (four?) different techniques (temporary heap backup, nothrow copy or move, nothrow default constructor). Only the last one (which is, unfortunately, the one invoked by your code), does not satisfy strong exception-safety. It's fairly funny how an optimization diminishes the safety of variant.
In my opinion the tradeoffs of basic exception guarantee in this case are bigger than usual: the behavior it brings is very similar to an object changing its type. These semantics are not compatible with the usual semantics of operator=. I understand that there are use cases where it is unreasonable to pay the price of strong exception guarantee, but those cases can be supported with a separate operation with a name that hints about the unusual, possibly type-changing semantics. Also, I am not necessarily advocating strong exception guarantee. I would be fine if the semantics of operator= were such that it may leave the object in a particular unusual state. The problem is that as it is now, it may leave the object in a seemingly OK state. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

AMDG Emil Dotchevski wrote:
Also, I am not necessarily advocating strong exception guarantee. I would be fine if the semantics of operator= were such that it may leave the object in a particular unusual state. The problem is that as it is now, it may leave the object in a seemingly OK state.
* "Implementation Note*: So as to make the behavior of |variant| more predictable in the aftermath of an exception, the current implementation prefers to default-construct |boost::blank| if specified as a bounded type instead of other nothrow default-constructible bounded types. (If this is deemed to be a useful feature, it will become part of the specification for |variant|; otherwise, it may be obsoleted. Please provide feedback to the Boost mailing list.)" In Christ, Steven Watanabe

On Mon, Aug 4, 2008 at 12:04 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
AMDG
Emil Dotchevski wrote:
Also, I am not necessarily advocating strong exception guarantee. I would be fine if the semantics of operator= were such that it may leave the object in a particular unusual state. The problem is that as it is now, it may leave the object in a seemingly OK state.
* "Implementation Note*: So as to make the behavior of |variant| more predictable in the aftermath of an exception, the current implementation prefers to default-construct |boost::blank| if specified as a bounded type instead of other nothrow default-constructible bounded types. (If this is deemed to be a useful feature, it will become part of the specification for |variant|; otherwise, it may be obsoleted. Please provide feedback to the Boost mailing list.)"
Yes, I was aware of this workaround and of the one Mathias mentioned. However, variant<foo,bar,blank> and variant<wrapper<foo>,wrapper<bar>> aren't the same as variant<foo,bar>. I'm questioning the rationale of the current variant::operator= semantics, not necessarily looking for a way to work around them. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

AMDG Emil Dotchevski wrote:
Also, I am not necessarily advocating strong exception guarantee. I would be fine if the semantics of operator= were such that it may leave the object in a particular unusual state. The problem is that as it is now, it may leave the object in a seemingly OK state.
Yes, I was aware of this workaround and of the one Mathias mentioned.
However, variant<foo,bar,blank> and variant<wrapper<foo>,wrapper<bar>> aren't the same as variant<foo,bar>. I'm questioning the rationale of the current variant::operator= semantics, not necessarily looking for a way to work around them.
It doesn't seem unreasonable to require boost::blank to be listed, in order to allow a variant to be left in a "particular unusual state" In Christ, Steven Watanabe

On Mon, Aug 4, 2008 at 12:48 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
AMDG
Emil Dotchevski wrote:
Also, I am not necessarily advocating strong exception guarantee. I would be fine if the semantics of operator= were such that it may leave the object in a particular unusual state. The problem is that as it is now, it may leave the object in a seemingly OK state.
Yes, I was aware of this workaround and of the one Mathias mentioned.
However, variant<foo,bar,blank> and variant<wrapper<foo>,wrapper<bar>> aren't the same as variant<foo,bar>. I'm questioning the rationale of the current variant::operator= semantics, not necessarily looking for a way to work around them.
It doesn't seem unreasonable to require boost::blank to be listed, in order to allow a variant to be left in a "particular unusual state"
It depends on the viewpoint. If you have operator= which may change the type of the stored value, it's quite reasonable to specify that if you list boost::blank as a valid type, it will be preferred over the other valid types. This still doesn't mean that it is desirable for operator= to be allowed to change the type of the stored object. If you list boost::blank as a valid variant, then there's nothing unusual about this state. The difference in meaning is subtle but nevertheless important: it's similar to a pointer that may legally be null, requiring the user to deal with this possibility, vs. a pointer which can never be null, allowing the user to assert when it is. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

AMDG Emil Dotchevski wrote:
This still doesn't mean that it is desirable for operator= to be allowed to change the type of the stored object.
In other words, you /are/ advocating the strong guarantee.
If you list boost::blank as a valid variant, then there's nothing unusual about this state. The difference in meaning is subtle but nevertheless important: it's similar to a pointer that may legally be null, requiring the user to deal with this possibility, vs. a pointer which can never be null, allowing the user to assert when it is.
I don't follow. IMO, unless you are making the exact variant type part of your class' public interface, it is irrelevant how you represent an empty variant. The only difference with adding boost::blank vs. creating some empty state, is that the presence of the empty state is explicit. In Christ, Steven Watanabe

On Mon, Aug 4, 2008 at 2:57 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
AMDG
Emil Dotchevski wrote:
This still doesn't mean that it is desirable for operator= to be allowed to change the type of the stored object.
In other words, you /are/ advocating the strong guarantee.
If you list boost::blank as a valid variant, then there's nothing unusual about this state. The difference in meaning is subtle but nevertheless important: it's similar to a pointer that may legally be null, requiring the user to deal with this possibility, vs. a pointer which can never be null, allowing the user to assert when it is.
I don't follow. IMO, unless you are making the exact variant type part of your class' public interface, it is irrelevant how you represent an empty variant. The only difference with adding boost::blank vs. creating some empty state, is that the presence of the empty state is explicit.
Look, if I have: variant<foo,int> a=foo(....); variant<foo,int> b=foo(....); It is my understanding that if I now do: a=b; I may end up with an int in a. Do you think that this behavior is reasonable? Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

AMDG Emil Dotchevski wrote:
Look, if I have:
variant<foo,int> a=foo(....); variant<foo,int> b=foo(....);
It is my understanding that if I now do:
a=b;
I may end up with an int in a. Do you think that this behavior is reasonable?
You won't just end up with an int in a. You will also end up with an exception. Yes. I think this behavior is reasonable. In Christ, Steven Watanabe

Steven Watanabe:
Emil Dotchevski wrote:
Look, if I have:
variant<foo,int> a=foo(....); variant<foo,int> b=foo(....);
It is my understanding that if I now do:
a=b;
I may end up with an int in a. Do you think that this behavior is reasonable?
You won't just end up with an int in a. You will also end up with an exception. Yes. I think this behavior is reasonable.
My naive expectation would be for the above code to call foo::operator=. This would of course imply that variant::operator= would require the types to be assignable. That aside, I think that it would be reasonable to expect the type to remain foo at least when foo has a nothrow default constructor.

AMDG Peter Dimov wrote:
Look, if I have:
variant<foo,int> a=foo(....); variant<foo,int> b=foo(....);
It is my understanding that if I now do:
a=b;
I may end up with an int in a. Do you think that this behavior is reasonable?
My naive expectation would be for the above code to call foo::operator=. This would of course imply that variant::operator= would require the types to be assignable.
It does call operator= when both operands have the same type. Thanks.
That aside, I think that it would be reasonable to expect the type to remain foo at least when foo has a nothrow default constructor.
Yes. It shouldn't add runtime overhead to try to fall back to the original type, if possible. In Christ, Steven Watanabe

On Mon, Aug 4, 2008 at 4:35 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
It does call operator= when both operands have the same type. Thanks.
Is it then true that even foo::operator= throws, in the code below: variant<foo,int> a=foo(....); variant<foo,int> b=foo(....); a=b; we will never end up with an int in a? If that's the case, then maybe I missed something in the documentation. Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

AMDG Emil Dotchevski wrote:
On Mon, Aug 4, 2008 at 4:35 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
It does call operator= when both operands have the same type. Thanks.
Is it then true that even foo::operator= throws, in the code below:
variant<foo,int> a=foo(....); variant<foo,int> b=foo(....); a=b;
we will never end up with an int in a? correct. If that's the case, then maybe I missed something in the documentation.
This behavior is stated here: http://www.boost.org/doc/libs/1_35_0/doc/html/boost/variant.html#id978547-bb The discussion here http://www.boost.org/doc/libs/1_35_0/doc/html/variant/design.html#variant.de... distinguishes between same-type and different-type assignments, too. In Christ, Steven Watanabe

On Mon, Aug 4, 2008 at 6:08 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
Emil Dotchevski wrote:
On Mon, Aug 4, 2008 at 4:35 PM, Steven Watanabe <watanabesj@gmail.com> wrote:
It does call operator= when both operands have the same type. Thanks.
Is it then true that even foo::operator= throws, in the code below:
variant<foo,int> a=foo(....); variant<foo,int> b=foo(....); a=b;
we will never end up with an int in a?
correct.
Cool, thanks! Emil Dotchevski Reverge Studios, Inc. http://www.revergestudios.com/reblog/index.php?n=ReCode

in a = b; , we can have exception-safe assignment if we have, or can simulate, nothrow move of either a's current type (move a to backup on stack first, then try copying b; if it fails, move a back from the stack, if it succeeds, destruct a-on-stack) or b's current type (copy b to stack first, and if that succeeds, destruct a and move the copy of b that's on the stack to a). The current implementation of variant tries to detect whether that nothrow move exists for a. It does for builtin types. If it does, then it uses a completely safe implementation. If it doesn't, it uses an implementation that still has a useful property: 1. If the type has nothrow copy-construction, we are completely safe, whether or not Boost.Variant can detect it. 2. It doesn't need nothrow copy-construction *in general*, it only needs to be able to copy-construct again something that was copy-constructed the first time, in particular something like: type a_copied(a); //may throw a.~type(); //it's in variant, so must be nothrow new (&a) type(a_copied) //we wish this to never throw Of course, 3. guaranteeing that with "interesting" types is prone to race conditions 4. many types, such as std::vector, use dynamic memory allocation, which in theory (and maybe in some real situations) may throw. However, things may not be as bad as they look. If a type has nothrow swap and a nothrow constructor (e.g. default-constructor) then we can simulate nothrow swap. However std::vector appears to have no nothrow constructors. So we're actually stuck even if just two of the types in the container are standard-container types. (although, arguably the default-constructor+swap is less likely to throw, so it could be attempted if it seems worth the complexity/impossibility of detecting default-constructibility, possibly as a fallback after copy-construction has thrown or vice versa) We can also simulate "move" by copying bits around, but that solution was rejected. After all, they would have to be bits representing a's type or b's type, and we don't have a lot of guarantees about that type. Types for which it's absolutely definitely safe to copy bits around, have nothrow copy-construction anyway. Any further hacks in this area really need to go in a Boost.Move library. Also, if we're willing to put up with possible variant contents on the heap when the type's copy-constructor threw, and in the hopeful policy-based future when we can actually say we want Variant's heap-behavior rather than the nothrow-default-construction behavior, we can get the type-preserving strong-exception-guarantee invariant, without any C++0x features. Looking to the future, why doesn't the draft C++0x standard's type-traits include: has_nothrow_move_constructor has_nothrow_move_assign has_nothrow_destructor ? With has_nothrow_move_constructor, we could implement the strong exception guarantee for assignment for all Variants that had at most one type that lacked a nothrow move-constructor. Without it, we can't be quite as smart, and could only guarantee strong exception safety for assignment when *all* the variant's types have nothrow move-construction. But if all the variant's types have nothrow move, then the variant itself has nothrow move. Did anyone come up with a type that legitimately has a move-constructor that can throw (even when the destructor can't throw, per Variant's existing requirements)? -Isaac
participants (7)
-
Emil Dotchevski
-
Eric Friedman
-
Isaac Dupree
-
Larry Evans
-
Mathias Gaunard
-
Peter Dimov
-
Steven Watanabe