Boost logo

Boost :

From: Douglas Gregor (gregod_at_[hidden])
Date: 2002-06-13 00:05:16


On Wednesday 12 June 2002 06:48 pm, Itay Maman wrote:
> 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.

If we don't understand the desired behavior well enough to determine a
reasonable semantics, then we clearly cannot understand all of the
interactions in a policy-based system. However, I don't believe this is the
case: variants are not a new concept, and collectively we've had a reasonable
amount of experience with different implementations of variants so that we
should be able to make good decisions.

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

I don't think that is a good example. An Allocator only changes the semantics
of a container in very subtle ways that won't affect 99% of C++ programs.
Some of the proposed policies affect the semantics a huge amount. For
instance, let's consider this:

  variant<TYPELIST_2(int, float), ConversionPolicyA> v1(3.14159);
  variant<TYPELIST_2(int, float), ConversionPolicyB> v2(3.14159);
  variant<TYPELIST_2(int, float), ConversionPolicyC> v3(3.14159);

Now, let's define the conversion policies in some of the ways discussed
before:
  ConversionPolicyA: if no exact type match, fail
  ConversionPolicyB: if no exact type match, pick first type that can work
  ConversionPolicyC: choose best type via overloading

The results for this experiment? v1 will fail to compile. v2 will hold the
integer value 3. v3 will hold the floating-point value (float)3.14159.
Compound this with a few other policies, and eventually you can't rely on any
sensible semantics for the generic 'variant' class.

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

I couldn't disagree more. When designing a new component, try to determine the
most appropriate semantics for that component. When expectations on the
semantics are wildly divergent (e.g., std::auto_ptr move semantics vs.
boost::shared_ptr reference-counted semantics), but in a way that can be
managed without code duplication by appropriate factoring, policies provide
one potential implementation technique. If you start out creating a
policy-based components, it is easy to come up with policies for every aspect
of that component, but whether each aspect truly deserves a policy is the
important question. We cannot foresee every use of a component, but neither
can we leave the component so open-ended that it has no concrete use.
Considering the set of proposed policies again:

  1) Storage
    This policy basically collapses in on itself, because there are only two
options to choose from, and everyone seems to agree that stack-based
allocation is highly preferable. Plus, there are easy ways to simulate heap
allocation using stack allocation (Andrei suggests putting a smart_ptr in the
variant, and I suggest an 'incomplete' designator, both of which are more
fine-grained and more obvious than writing a new policy).

  2) Automatic casting (implicit conversion from types to a variant)
    There are really only two options here: never allow implicit conversion or
perform overloading to determine the best conversion. The two other policies
are essentially weak forms of overload resolution that are doomed to confuse
users because they have no C++ analogue. So, given that there are already
ways in C++ to suppress implicit conversions, why do we need a policy here?

  3) Assignment error
    I think we agreed (?) that this policy isn't needed, because anything
other than a compile-time error wouldn't make sense for C++.

  4) Variant-to-variant assignment error:
    Same as #3?

I just don't see any axis along which there is a significant variation with
the use of variants where we would need a nontrivial policy.

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

If you're familiar with the Lambda library, here is a short description of the
semantics of the assignment v2 = v1 for variants v1 and v2 (that need not be
the same type of variant):

  v1.visit(ref(v2) = _1);

This means: for each type that v2 may contain, find the best type storeable in
v1 (i.e., via overload resolution) and implicitly convert to that type when
copying.

For your example above, the logic of "v2 = v1" looks something like this:
  the best type in v2 if v1 is storing an int is int
  the best type in v2 if v1 is storing a Negate<t_var> is Negate<t_var>

so when v1 holds an int, v2 will get that int value. Things get more
interesting when the variant types are different (but the definition remains
the same), e.g.,

  variant<int, float, std::string> v1;
  variant<long, char*, double> v2;

  v1 = v2;

If v2 is storing a 'long', then it should be converted to an 'int' to be
stored in v1
If v2 is storing a 'float', then it should be converted to a 'double' to be
stored in v1
If v2 is storing a 'char*', then it should be converted to an 'std::string' to
be stored in v1

The rules are order-independent, and IMHO quite logical. If there is a 'best'
assignment from a variant to another variant, use it. If not, the compiler
will complain.

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

I'm not sure I understand why we would want this. What are the semantics of
'as_variant'?

        Doug


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