Boost logo

Boost :

From: Douglas Gregor (gregod_at_[hidden])
Date: 2002-06-12 10:15:14


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

> > 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.

> > 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 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.

> > 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.

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 :)

> >
> > ~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:

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.

        Doug


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