Boost logo

Boost :

From: Itay Maman (itay_maman_at_[hidden])
Date: 2002-06-12 17:48:05


Douglas Gregor wrote:
[snip]
>>
>>What about automatic casting from non-specified types?
>>(For example: initializing a union of <float, string>
>>with a double value). Personally, I think this
>>behavioral detail should be specified by a policy.
>
>
> I agree that we need implicit conversions to types stored in the
union (e.g.,
> your variant<float,string> implicitly constructed from a double
example), but
> I don't agree that this should be a policy. The reason will be discussed
> below where it comes up again.
>
>
>>In general, I see four separate, policy-controlled,
>>aspects:
>
>
> I try to avoid policy-based designs when possible, because they
introduce a
> large amount of variance in the semantics of a single component. I'll
discuss
> each of the four potential policies below:
>
>

The main advantage of the policy-based approach is that it makes the
discussion we are having now, regarding: "How should the variant class
behave in situation XXX ?", totally irrelevant. I belive that we cannot
pre-decide on a beahvior, which will suite the needs of every client
code to be written. We simply cannot foresee all the cirumstances under
which variants will be used.

Policy-based designs do tend to produce a "large amount of variance",
however, this large amount does not strike me as overwhellming: There's
   no confusion about the semantics of std::vecotr, despite the fact that
it has at least one degree of freedom (i.e: the Allocator template
parameter)

Consequently, We must first decide whether we'd like to see a
policy-based design in variant's implementation. (I do).

Only after we answer this question, we should go on to either select
which policies should our variant have, or to debate over variant's
concrete behavior under a common usage profile.

>>1) Storage: (i) boost::any (ii) on-stack
>
>
> When does one prefer the boost::any storage method instead of the
on-stack
> method? The on-stack approach is a lightweight approach that closely
> resembles C-style unions; in the cases where variant objects are too
large to
> put on the stack, the variant objects can easily be allocated on the
heap
> (just like we would do with a C-style union). The only cases where
on-stack
> allocation isn't feasible occur with recursive or incomplete types
(discussed
> later...). So, I would say that we should only use on-stack
allocation but
> allow an 'escape' to heap allocation for recursion and incomplete types.
>
Stack allocation should be the default, since it is quicker and safer.

>
>>2) Automatic casting: (i) never, (ii) cast to the
>>first type possible, (iii) cast if only one possible
>>type found
>
>
> How about:
> (iv) use overload resolution to choose the best type, or fail if no
best
> type exists
Seems like a very useful option.

>
> 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.
> (ii) this is very unnatural. You could end up with something like:
> variant<int, float> v;
> v = 3.14159; // v stores the integer 3
Well, (ii) is not defined well. I should have written: "(ii) use exact
match. If not found, cast to the first type possible"

> (iii) this is reasonable, but again: overload resolution is already
> understood as a mechanism for finding the best alternative, so why
not reuse
> it?
>
> Side note: the desire for overload resolution is why I am unsure of a
purely
> typelist-based approach. Getting all of the right overload candidates
there
> given just a typelist is horrific.
>
>
>>3) Assignmenmt error: (i) Compile-time error, (ii)
>>initialize with default value of first type
>
>
> I don't understand why (ii) would ever be desirable. We should be
checking for
> errors as early as possible.
Agreed. (ii) is a real troublemaker.

>
>
>>The fourth aspect relates to the issue of variant to
>>variant assigmnet:
>> Variant<List1> u1;
>> Variant<List2> u2;
>> ...
>> u1 = u2; //Problem(?): u2's held type may be
>>invalid
>> // for assignment into u1
>>
>>
>>4) Variant to variant assigmnet error: (i)
>>Compile-time error if List2 is not a sub set of List1.
>>(ii) Run-time error (iii) assign the default value of
>>the first type on List1
>
>
> How about:
> (iv) compile-time error if for any of the types of the source
variant there
> is no 'best candidate' type in the target variant. So something like
this
> should be fine:
>
> variant<int, float, double> v1;
> variant<long, double> v2;
> v1 = v2; // okay
>
> Again, I can't see a reason for using (ii) or (iii) because we should be
> checking for errors as soon as possible. The behavior of (iii) could
drive
> one positively mad during a late-night debugging session. (i) is
okay, but
> overly strict if we want implicit conversions (I do).

Again, agreed.

>
[snip]

>
> From the implementation point of view, recursive types aren't hard to
deal
> with: just don't try to allocate them on the stack. The complexity is
stating
> the recursion when using the variant type. My personal preference is
to use a
> pseudo-keyword 'rec', e.g.,
>
> typedef variant<int, variable_t*, Plus<rec, rec>, Negate<rec> > expr;
>
> As for incomplete types, refer back to the C-style union version of
'expr'.
> The variable_t type doesn't need to be defined because we're only
holding a
> pointer to it. If we could specify that a particular type is incomplete
> (therefore requesting heap allocation), then we could easily use
incomplete
> types with the variant. It might look like this:
>
> typedef variant<int, incomplete<variable_t>, Plus<rec, rec>,
Negate<rec> >
> expr;
>

Well, when I read your previous post I was thinking something like: "I
don't understand it, unless he speaks of some pseudo-keywords" :)

[snip]
As for the visitor, you got me conviced: a static visit_at() member
function does not offer any real benefit, and, the 'go()' construct is
merely just a syntactic sugar.

>
> Following is a partial sketch of the interface I have in mind. I've
omitted
> details that we haven't really discussed yet or that I haven't
thought about
> long enough to have a strong opinion on (tagged types, recursive types,
> stating type incompleteness, etc.).
>
> Doug
>
> 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
>
> ~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)
> bool empty() const { return false; } // boost::any compatibility
> };
>
> struct bad_variant_cast : public std::runtime_error { /* ... */ };
>
> // cast to type T if current value is of type T; otherwise, throw
> // bad_variant_cast
> 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);
>

There is one case which needs some attention:

(Suppose Negate<T> can be implicitly constructed from an integer)

    typedef variant<int, Negate<rec> > t_var;
    t_var v1, v2;
    v1 = 4; // v1 holds an int
    v2 = v1; // v2 ???

What is the held type of v2 after the assigmnet: Is it int or
Negate<int> ?

I think that the most natural solution is to have another pseudo-keyword
   which will make the above assignment to take the rhs as a variant,
rather than taking its held value:

    typedef variant<int, Negate<rec> > t_var;
    t_var v1, v2;
    v1 = 4; // v1 holds an int
    v2 = as_variant(v1); // v2 holds Negate<t_var>

-Itay


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