Boost logo

Boost :

From: Aleksey Gurtovoy (agurtovoy_at_[hidden])
Date: 2004-03-15 03:13:29


Matt Calabrese wrote:
> Okay, I decided before I continue with my other library, I'll fully
> implement rational_c properly (and possibly fixed_c), however, doing
> so without making the code horribly complex would require changes to
> the current definitions of pretty much all of the operations (plus,
> multiplies, etc. as well as the conditional operators).
                                  ^^^^^^^^^^^
I suppose you meant "comparison", here and below.

>
> The problem is that those current functions take 5 parameters, making
> specialization nearly impossible for the rational and fixed templates
> as you'd have to account for when the 3rd, 4th, 5th, etc parameters
> are not passed (and so the default integral 0 is used). That can't be
> easily done (at least I don't see how) without having to specialize
> each condition separately (one for 3 rational types, one for 4
> rational types, etc ). Moreso, this applies to the conditional
> operators as well, there is no easy way to account for using different
> types in multiplies IE multiplying a rational and integral together,
> or a fixed and rational, etc. Again, to account for these
> possibilities you'd have to make an incredibly large amount of
> specializations. Of course, you could always just limit them to only
> work when all the parameters are the same kind of compile-time value
> template, but that would be somewhat limitting,especially with other
> solutions available.

I pretty much agree with the analysis.

>
> What I propose is a way around this situation, but still have the
> metafunctions declared the same way, therefore not breaking old code
> and not limiting functionality. To do so, the following adjustments
> would have to be made (and I have already begun to make them for my
> own use):
>
> 1) Additional versions of all of the arithmetic operators which take 2
> parameters, each need to be defined and work like binary operators.
> They should be specialized for when both the left and right operands
> are of the same type. How to account for when they are different types
> will be covered in the future points.

Sounds good so far.

> 2) A value_cast metafunction which can cast between all different
> compile-time constant types. The way it is implemented is by requiring
> all types to have specialized metafunctions for converting to and from
> the fixed point type. When value_cast is used, it, by default,
> converts the right hand operand to fixed_c, then converts that fixed_c
> to the target type. Since a fixed_c can hold pretty much any real
> value to a certain precision, you can be certain, to an extent, that
> little, if any precision will be lost through conversion. So, with
> that proposed addition, you'd be able to convert between all different
> types and only require people developing new types to have to
> specialize conversion to and from fixed_c. The only problem is that in
> order for the value_cast to exist, it obviously needs to have a
> parameter which specifies a type to convert to. The only way I can
> currently see that would allow that would be to have the value_cast
> take a type parameter which would be a template instantiation with a
> dummy value, which would, in a way, be very similar to rebinding
> allocators.

The idea of avoiding a combinatorial explosion through a single
user-defined conversion metafunction which will be used to even the
argument types is definitely solid; however, I am afraid we cannot
escape with something as simplistic as conversion to a default
predefined "contains-it-all" numeric type -- simply because there is no
such type (for instance, 'fixed_c' is not capable of holding
compile-time complex numbers, and so on). Moreover, specializing of
arithmetic metafunctions is not necessary a prerogative of numeric types
-- consider, for example, random-access iterators.

To summarize, IMO the idea is excellent, but it needs to be carried a
little bit further -- please see below.

> 3) The conditional operations and 2 operand arithmetic operations
> should be redefined in the base template definition to convert both
> types to fixed_c and then make a comaprison, therefore allowing
> programmers to use the operations on two different types of
> compile-time constants without having to manually convert them to
> similar types before-hand (as I alluded in first point). The
> conditional operations can then be specialized for when the types
> are the same on each side to avoid the cost of a cast on each of the
> operands to fixed_c.

Yep to everything except the 'fixed_c' part.

> 4) The current "varying length" operations which cause problems
> (multiplies, plus, minus, etc) should be redefined to just call the 2
> operand version of the corresponding operation on each of the
> parameters passed to the operation. This way, programmers can still
> pass multiple parameters to these functions, with each parameter being
> a different type, without the developer having to make an extremely
> large amount of specializations for each type (including those which
> have yet to be developed).

Yep! Something like

    template<
          typename N1
        , typename N2
        , typename N3 = int_<0>
        , typename N4 = int_<0>
        , typename N5 = int_<0>
>
    struct plus
        : plus2< plus2< plus2< plus2<N1,N2>,N3 >,N4 >,N5 >
    {
    };

    // ...

    template<
          typename N1
        , typename N2
>
    struct plus<N1,N2,int_<0>,int_<0>,int_<0> >
        : plus2<N1,N2>
    {
    };

>
> I have already begun implementing all of the points I have suggested,
> though with my rational_c as a the basis for conversion rather than
> fixed_c (as I haven't yet developed fixed_c). Also, converting to and
> from rational_c is considerably simpler than converting to and from
> fixed_c. For this reason, I may end up leaving the basis for
> conversion as rational_c instead of fixed_c.
>
> If anyone sees any other, more elegant solutions for the problems I
> mentioned, please post them and I may change my approach. Additional
> suggestions are also greatly appreciated.

You are going in the right direction. Here are some suggestions of how
to generalize the scheme to get what I would consider an ideal solution:

1) Make specializable binary templates for each arithmetic/comparison
metafunctions to be tag-dispatched metafunction classes, e.g.

    template<
          typename N1
        , typename N2
>
    struct plus2
        : plus_impl< // 'plus_impl' is a point of customization
              typename plus_tag<N1>::type
            , typename plus_tag<N2>::type
>::template apply<N1,N2>::type
    {
    };

    // 'plus_impl' implemenation for plain integral constants
    template<>
    struct plus_impl<integral_c_tag,integral_c_tag>
    {
        template< typename N1, typename N2 > struct apply
            : integral_c<
                  BOOST_MPL_AUX_TYPEOF(N1,N1::value + N2::value)
                , (N1::value + N2::value)
>
        {
        };
    };

where 'plus_tag' is something as simple as

    template< typename T > struct plus_tag
    {
        typedef typename T::tag type;
    };

2) Provide a rank mechanism so that we know in which direction to cast
in case of mixed operations; can be as simple as an integral-constant
'rank' member of the corresponding tag:

    plus_tag< int_<5> >::type::rank == 0

3) Provide default implementations of '*_impl' binary templates which
would use ranks, tags, and 'numeric_cast' to implement a general scheme
for mixed arithmetics/comparison, something along these lines:

    template<
          bool c_
        , typename Tag1
        , typename Tag2
>
    struct plus_impl_
    {
        template< typename N1, typename N2 > struct apply
            : plus_impl<Tag2,Tag2>::template apply<
                  typename apply< numeric_cast<Tag2,Tag1>, N2>::type
                , N2
>
        {
        };
    };

    template<
          typename Tag1
        , typename Tag2
>
    struct plus_impl_<false,Tag1,Tag2>
    {
        template< typename N1, typename N2 > struct apply
            : plus_impl<Tag1,Tag1>::template apply<
                  N1
                , typename apply< numeric_cast<Tag1,Tag2>, N2>::type
>
        {
        };
    };

    template<
          typename Tag1
        , typename Tag2
>
    struct plus_impl
        : plus_impl_< ( Tag1::rank < Tag2::rank ),Tag1,Tag2 >
    {
    };

Does it make any sense to you?

P.S. I suggest we move the discussion to the developers list (I'm going
to cross-post this one).

--
Aleksey Gurtovoy
MetaCommunications Engineering

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