Boost logo

Boost :

From: Aleksey Gurtovoy (alexy_at_[hidden])
Date: 2001-05-21 10:44:22


Vesa Karvonen wrote:
> Treating the first element as a special case is one way to
> solve the problem
> of creating lists. It has the advantage that the enumeration macro
> implementation is slightly simpler. On the other hand, the
> user then needs
> to implement both the general and the special case macros.
>
> The LIST() primitive allows to use both a S[EPARATOR] and an
> E[LEMENT] and
> then expands to the following sequence:
>
> E(0) S(1) E(1) S(2) E(2) ... S(n) E(n)
>
> This solves two problems:
> - It solves the comma separated list generation problem
> - It allows starting from either 0 or 1
>
> For the above reasons, I find the approach taken by LIST,
> that is to have
> both a SEPARATOR and an ELEMENT, to be the most elegant approach.

The algebra is indeed quite elegant, I agree. Still, as a potential user of
the library, I am concerned about lost in readability and genericity
comparing to the technique I use now:

1) current 'boost::mpl' code:

template< typename T
        , BOOST_MPL_ENUMERATE_DEFAULT_PARAMS(class C, null_argument)
>
struct switch_on : mpl::detail::switch_impl<
                     T, mpl::type_list<BOOST_MPL_ENUMERATE_PARAMS(C)>
>::result_type
    {
    };

2) the same code implemented using current CPP approach

// this line does not count; it would be hidden in some internal
'boost::mpl' library's header
#define BOOST_MPL_ENUMERATE(separator, expression) \
BOOST_LST0(15, separator, expression)

// an equivalent to the (1)

#define DEFAULT_CLASS_PARAMETER_FUN(i) class C##i = null_argument
#define ARGUMENT_FUN(i) A##i

template<typename T
         BOOST_MPL_ENUMERATE(BOOST_COMMA_FUN, DEFAULT_CLASS_PARAMETER_FUN)
>
struct switch_on : mpl::detail::switch_impl<
    T, mpl::type_list<BOOST_MPL_ENUMERATE(BOOST_COMMA_FUN, ARGUMENT_FUN)>
>::result_type
  {
  };

#undef ARGUMENT_FUN
#undef DEFAULT_CLASS_PARAMETER_FUN

One particular loss I don't like here is loss of the ability to specify
arguments of the macro invocation "in place", i.e.

#define DEFAULT_CLASS_PARAMETER_FUN(i) class C##i = null_argument
BOOST_MPL_ENUMERATE(BOOST_COMMA_FUN, DEFAULT_CLASS_PARAMETER_FUN)
#undef DEFAULT_CLASS_PARAMETER_FUN

instead of

BOOST_MPL_ENUMERATE_DEFAULT_PARAMS(class C, null_argument)

I consider this a key feature of the technique, which I would like to see
kept somehow (parametrization of BOOST_LST0 seems to be a good option, but
see below).

Also, although at first sight providing separate primitives for generating
argument lists (which would allow "in place" arguments specification) might
seem as a viable solution for this, IMO it's not, because such primitives
cannot be implemented using original BOOST_LST# mechanism, so as soon as one
will need something that won't be covered by them, (s)he will have to resort
to the second (#2 above) approach and, as result, deal with code that does
similar things in two different ways.

Another issue I am concerned about is the way generation of both kinds of
parameters lists (starting with 0 and starting with 1) is achieved:

#define ARG(i) T##i
BOOST_LST0(5, BOOST_COMMA_FUN, ARG)
#undef ARG

generates "T0, T1, T2, T3, T4", but

#define ARG(i) T##i,
BOOST_LST0(5, ARG, BOOST_EMPTY_FUN)
#undef ARG

generates "T1, T2, T3, T4".

IMO it's very tricky, and personally, without looking at BOOST_LST0
definition, I would never remember which way I should write something like
this to get the desired effect.

I agree, however, that ability to do this kind of things is very important.
I just prefer to have a little bit more intuitive way to do it, even at the
price of writing more auxiliary code:

// somewhere in the internal header
#define BOOST_MPL_PARAMETER0(param, unused)
#define BOOST_MPL_PARAMETER1(param, unused) param##1
#define BOOST_MPL_PARAMETER(n, param, unused) , param##n
#define BOOST_MPL_ENUMERATE_PARAMS(n, param)
BOOST_MPL_ENUMERATE_MACRO_CALLS( \
    n, BOOST_MPL_PARAMETER, param, unused \
    )

#define BOOST_MPL_ZERO_BASED_PARAMETER0(param, unused) param##0
#define BOOST_MPL_ZERO_BASED_PARAMETER1(param, unused) , param##1
#define BOOST_MPL_ZERO_BASED_PARAMETER(n, param, unused) , param##n
#define BOOST_MPL_ENUMERATE_PARAMS_FROM_0(n, param)
BOOST_MPL_ENUMERATE_MACRO_CALLS( \
    n, BOOST_MPL_ZERO_BASED_PARAMETER, param, unused \
    )

// in client code
BOOST_MPL_ENUMERATE_PARAMS(5, T) // T1, T2, T3, T4, T5
BOOST_MPL_ENUMERATE_PARAMS_FROM_0(5, T) // T0, T1, T2, T3, T4, T5

> Two extra parameters
> ====================
>
> As a design heuristic, allowing extra parameters may seem
> like it would make
> the list more generic, but in my experience most
> "generalizations" that make
> the implementation more complicated have turned out to be
> specializations.
>
> A possible merging would be to allow giving parameters to the
> SEPARATOR and
> ELEMENT macro:
>
> #define LIST(n,S,E,P0,P1) CAT(LIST,n)(S,E,P0,P1)
> #define LIST0(S,E,P0,P1)
> #define LIST1(S,E,P0,P1) E(0,P0,P1)
> #define LIST2(S,E,P0,P1) LIST1(S,E,P0,P1) S(1,P0,P1) E(1,P0,P1)
> #define LIST3(S,E,P0,P1) LIST2(S,E,P0,P1) S(2,P0,P1) E(2,P0,P1)
> // ...
>
> This has the advantage that certain common situations could
> be implemented
> in the CPP library code. For example:
>
> #define ARG_LIST_WITH_A_DEFAULT(n,NAME,THE_DEFAULT)\
> LIST(n, COMMA_FUN, ARG_WITH_A_DEFAULT, NAME, THE_DEFAULT)
> #define ARG_WITH_A_DEFAULT(i, NAME, THE_DEFAULT) NAME##i
> = THE_DEFAULT
>
> #define ARG_LIST(n,NAME)\
> LIST(n, COMMA_FUN, ARG, NAME, unused)
> #define ARG(i, NAME, unused) NAME##i
>
> There is at least one complication however. Namely, if the NAME or
> THE_DEFAULT have commas, the above technique break down. This
> is one of the
> reasons why the original LIST specificly only accepts macros
> as arguments.

I don't think it's a significant problem. In case of non-trivial macro
arguments users can always write their own case-specific (but high-level)
primitives using the standard ones as an example:

#define MY_FUNCTION_ARG(i, param_type, param_name) \
param_type##i param_name##i = some_traits<param_type##i, i>::default_value

#define MY_FUNCTION_ARG_LIST(n, param_type, param_name) \
BOOST_LST0(n, BOOST_COMMA_FUN, MY_FUNCTION_ARG, param_type, param_name)

template<ARG_LIST(10, typename T)>
struct something
    {
    void foo(MY_FUNCTION_ARG_LIST(10, T, t));
    };

#undef MY_FUNCTION_ARG
#undef MY_FUNCTION_ARG_LIST

An important thing here is that these custom primitives still follow the
same usage pattern as the predefined ones.

>
> Anyway, you could then use them like this:
>
> template
> < ARG_LIST_WITH_A_DEFAULT(
> MAKE_LIST_MAX_LENGTH,
> class T,
> NS(Private::)Make_List_End) >
> struct Make_List
> {
> // Unfortunately, you would then need distinct
> versions in order
> // to use parameter shifting.
> Make_List
> < ARG_LIST_FROM_1_TO(MAKE_LIST_MAX_LENGTH, T) >
> // ...
> };
>
> I think that this has certain appeal, because you can then
> keep most of
> #define's out of sight and the code is perhaps a bit easier
> to read, because
> it doesn't have preprocessor statements, which don't fit into
> the regular
> C++ syntax.

IMO it's a lot easy to read :). just my opinion, though :).

> On the other hand, the extra parameters are often redundant.
> They seem to be
> useful only for generating argument lists.

I would say they are useful every time you want to explicitly and "in place"
specify a name of one or another part of the generated expression. For some
reason I always want to do that :).

Anyway, thank you for very good and detailed analysis of the issues! I think
that now I understand the tradeoffs of both approaches; need to think a
little bit more about a good way to merge them, though (certainly, they need
to be merged), so more thoughts to come :).

Also, if anyone else has comments/preferences - please, speak up!

Aleksey


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