Boost logo

Boost :

From: Fernando Cacciola (fcacciola_at_[hidden])
Date: 2002-06-12 12:18:10


----- Original Message -----
From: "Douglas Gregor" <gregod_at_[hidden]>
To: <boost_at_[hidden]>
Sent: Wednesday, June 12, 2002 12:15 PM
Subject: Re: [boost] Proposal --- A type-safe union

> On Wednesday 12 June 2002 10:01 am, Fernando Cacciola wrote:
> [lots of snipping going on]
> > > > 2) Automatic casting: (i) never, (ii) cast to the
> > > > first type possible, (iii) cast if only one possible
> > > > type found
> >
> > What means 'automatic' casting? If it means implicit conversions, I
think
> > this could be achievable by having the set of operator Tn() in the
variant.
>
> I thought by 'automatic' casting Itay meant 'implicit conversion to a
variant
> type', e.g.,
>
> void foo(const variant<int, float>& v);
> // ...
> double pi = 3.14159;
> foo(pi); // okay, stored as a float in v
>
Aha, I see. I though he referred to exactly the opposite. :-)

>
> > > How about:
> > > (iv) use overload resolution to choose the best type, or fail if no
> > > best type exists
> >
> > Maybe I didn't understand point (2), but how would we use overload
> > resolution and what for?
>
> Overload resolution would be used to choose the best storage type within
the
> variant given an object of a type that isn't in the variant. The example
> above would choose to store the double as a float because that is a better
> match than storing the double as an int.
>
Yes, I see it now in this context.

>
> > > Again, I don't see a need for a policy here. Overload resolution is a
C++
> > > feature that every C++ programmer is aware of and it seems the natural
> > > solution for this component. Here are the reasons why I reject the
other
> > > three:
> > >
> > > (i) implicit conversion is a feature of C++. Suppressing implicit
> > > conversions should be performed by user types, not by the variant,
> > > because the set of implicit conversions for a type is a property of
the
> > > type
> >
> > itself,
> >
> > > not a container of that type.
> >
> > I agree that a variant should be implicitely convertible to any of its
> > types.
>
> We're talking about different things here :)
>
> FWIW, I believe that we should allow implicit conversion to a variant from
any
> type, but that we should NOT allow implicit conversion from a variant to
one
> of its types.
>
> Implicit conversion from a variant to any of its types strikes me as being
> very, very dangerous. Extracting a value of a specific type from a variant
is
> a dangerous operation, because one needs to check first if that type is
> actually the stored type. Now, invoke that dangerous operation using
implicit
> conversion (the user may not even realize a conversion is happening!) and
I
> think we've whipped up a recipe for run-time disaster.
>
I normally consider implicit conversion very dangerous and I usually avoid
them.
It appears that I've considered this variant type to be modeling a different
thing:

I thought of variant<T,U,V> as a type whose value is *equally* any of T, U
or V.
But this variant seems to be a type whose value is *only one of* T,U, V (but
not all at the same time).
(Now that I think of it, 'discriminated' would mean just that, wouldn't it?)

Assuming I was just wrong about the whole idea of a variant, all you've said
about implicit conversions is totally right.

>
> > I don't know about incomplete types.
> > In my view, T* is not an incomplete type; it is a perfectly complete
type
> > of pointer type.
> > If I put a pointer to T in a discriminated union, I want the 'pointer',
not
> > the 'T' instance, to be represented by the union.
> > That is, the union deals with the pointer value only; to whatever it is
> > pointing to, and how that is allocated, is IMO, totally unrelated.
> > I don't think there is a really good reason to have a truly-incomplete
type
> > in an union.
>
> My example could have been better :) Pointer types are indeed types.
However,
> it seems that it would be useful to be able to store an incomplete type
> without resorting to pointers. Basically, you're asking the variant to
store
> the type like it is a pointer, but hide the fact that it's a pointer from
the
> user of the variant.
>
> It could help with recursive types where the 'rec'
> approach is clumsy, for instance:
>
> typedef variant<incomplete<IfThenElse>,
> incomplete<WhileLoop>,
> incomplete<ExpressionStatement> > Statement;
>
> Here, IfThenElse, WhileLoop, and ExpressionStatement will all be classes
that
> will contain Statements, so they can't be defined until Statement is
defined.
> Essentially, they are all recursive types that aren't defined using 'rec'.
It
> would be a pity to have to deal with pointers in a case like this.
>
> > As for recursive types, the tag approach (using 'rec') would allow
variant
> > to be fully stack-based. No need for heap allocation within variant.
>
> There's more trickery in the 'rec' approach that I didn't describe.
Extending
> the above example, we might want to represent a block of statements using
an
> std::vector, e.g.,
>
> typedef variant<incomplete<IfThenElse>,
> incomplete<WhileLoop>,
> incomplete<ExpressionStatement>,
> std::vector<rec> > Statement;
>
> Now, std::vector<rec> isn't really useful because it can't possibly act
like
> std::vector<Statement>, which is what we really wanted. The trick is to
make
> 'rec' just a placeholder that will be replaced by the actual type when the
> variant is used. So Statement actually stores a block of statements as
> std::vector<Statement>, and the user sees it as std::vector<Statement>.
>
> This type of thing may or may not require heap allocation. For an
std::vector,
> stack allocation will still work because std::vector<Statement> doesn't
> depend on the size of Statement, but for other types, e.g.,
> std::pair<Statement,Statement>, the size does depend on the size of
> Statement, so we must use heap allocation. We can't tell if an arbitrary

> class template bases its size on its template parameters, so we have to be
> pessimistic and assume that heap allocation is required for recursive
types.
> Of course, we could supply a 'complete' wrapper so the user can state that
> the object size is independent of the variant type size, e.g.,
>
> typedef variant<incomplete<IfThenElse>,
> incomplete<WhileLoop>,
> incomplete<ExpressionStatement>,
> complete<std::vector<rec> > > Statement;
>
> IfThenElse, WhileLoop, and ExpressionStatement objects will be
heap-allocated
> whereas std::vector<Statement> will be stack-allocated.
>
OK, I see it.
I notice that there is a lot complexity just to support incomplete types and
full-featured recursion.
Incomplete types "could" be detached from the variant itself by an
appropiate handle-body scheme.
It does involve more work for the user, but it also unbound all the possible
decisions (allocation,ownership, etc...).

I think that a variant type supporting incomplete types *might* be useful.
Though, I still need to see a complete example were a handle-body pattern is
not better from an engineering perspective.
But OTOH, I think it will introduce significant complexity. It might be a
good idea to at least have a light-weight variant too.

> > > template<typename T1, typename T2 = unused2, ..., typename TN =
unusedN>
> > > class variant
> > > {
> > > public:
> > > variant(); // default-construct value of type T1
> > >
> > > variant(const T1& t1); // assign value t1
> > > variant(const T2& t2); // assign value t2
> > > // ...
> > > variant(const TN& tN); // assign value tN
> >
> > I'd like you to elaborate on the need of an extensive-list instead of a
> > dot-list.
>
> It's not an absolutely need, but it does make things easier. See how each
of
> the possible types is listed in a separate constructor? When a variant is
> constructed from any type, function overloading is used to select the most
> appropriate storage type in the variant for the incoming object. With a
> typelist/dot-list approach, you can't just list all of the constructors so
> you are forced to deal with overloading yourself.
>
It seems to me that the choice of extensive-list vs dot-list is driven by an
implementation concern.
So, from the user perspective, is there any significant difference? Would it
be better for the user to use a dot-list?
I think not, but still this desicion should not be so much affected by the
choice of matching mechanism; it should be based primary on the effect on
the interface.

> If you are really interested, there is a way to make the compiler perform
> overload resolution for an arbitrary-length typelist, but I'm not going to
> type out an explanation unless someone asks specifically :)
Well... You bet I'm curious... but I wouldn't ask you to explain it just to
satisfy my curiousity :-)
If you ever do explain it, just let me know.

>
> > >
> > > ~variant();
> > >
> > > void swap(variant& other); // swap values
> > >
> > > variant& operator=(const T1& t1); // assign value t1
> > > variant& operator=(const T1& t1); // assign value t1
> > > // ...
> > > variant& operator=(const TN& tN); // assign value tN
> > >
> > > variant& operator=(const variant& other); // assign value from other
> > >
> > > // semantics described above
> > > template<typename OT1, typename OT2, ..., typename OTN>
> > > variant& operator=(const variant<OT1, OT2, ..., OTN>& other);
> > >
> > > const std::type_info& type() const; // typeid(type of current value)
> >
> > What is the 'current' value of a variant?
>
> It's accessible via 'variant_cast', which I had previously defined as:
>
So.. If I got it right, this variant has -at a given time- a value of 'one'
of Tn, correct?

> template<typename T, typename T1, typename T2, ..., typename TN>
> T& variant_cast(variant<T1, T2, ..., TN>& v);
>
> template<typename T, typename T1, typename T2, ..., typename TN>
> const T& variant_cast(const variant<T1, T2, ..., TN>& v);
>
> > I'll add:
> >
> > operator T1 const& () const ;
> > operator T1& () ;
>
> I stated my objection to this earlier, but I should probably say a little
more
> about what features I hope to see for accessing the value in a variant. As
I
> see it, there would be three mechanisms for extracting values from a
variant:
>
> 1) variant_cast: this one is the least safe, because there is only the
weak
> guarantee that it will throw an exception if it is used to try to cast to
a
> value type that isn't currently stored. It's mainly here for boost::any
> compatibility.
>
> 2) a visitor mechanism: this one is completely safe, because at
compile-time
> it is ensured that there is a visitor function overload that can accept
each
> of the stored value types of the variant. We discussed the visitor
earlier.
>
> 3) a typeswitch mechanism: this one is also completely safe, because we
can
> check at compile-time if every possible stored value type is handled by
the
> switch statement. It could also be extended to support pattern matching.
>
> So with two typesafe options for extracting values from a variant, I don't
see
> a need for the unsafe implicit conversions.
>
>
Yes, I understand (I think:-)
The key point is that the value is of only one of the possible types, not
all of them simultaneously.

Regards,

Fernando Cacciola
Sierra s.r.l.
fcacciola_at_[hidden]
www.gosierra.com


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk