Boost logo

Boost :

Subject: Re: [boost] [preprocessor] Warning: Incoming
From: Paul Mensonides (pmenso57_at_[hidden])
Date: 2011-07-01 20:58:50


On Fri, 01 Jul 2011 10:40:37 -0400, Lorenzo Caminiti wrote:

> On Fri, Jul 1, 2011 at 2:09 AM, Paul Mensonides <pmenso57_at_[hidden]>
> wrote:

>> IS_VARIADIC( int x, int& y   ) // 0
>> IS_VARIADIC( (int x)(int& y) ) // 1
>> IS_VARIADIC( (...) xyz       ) // 1
>>
>> For your particular case, wouldn't that be sufficient?
>
> Yes, in fact IS_VARIADIC is the macro Boost.Local currently uses. See my
> rudimentary implementation at:
> http://svn.boost.org/svn/boost/sandbox/local/boost/local/aux_/
preprocessor/variadic/is.hpp
>
> This implementation is fine for Boost.Local but I have not tested in
> general and it might impose unacceptable limitation (like the empty
> issue we discussed also while discussing VMD).

IS_VARIADIC can be defined without the issues associated with IS_EMPTY.
The problem with the mythical IS_TUPLE is that it requires both
IS_VARIADIC *and* IS_EMPTY and that is where the problems come in.

>> Regardless, I actually believe that the (1) case above is actually a
>> large step *backwards*.  It's worse, not better, because it's trying
>> (and failing) to be the underlying language and introducing a bunch of
>> gotchas in the attempt.
>
> I can only say that (i) I agree with you BUT (ii) my library users (my
> customers) wanted (1) very badly so I really have no choice.

I know that in your particular case, it is already too late given that it
is a deployed interface. However, the better strategy would be to let
the third party make the (mistake of creating the) mapping. Those
responsible for writing code should do it the right way as far as can be
known at the time. For a library author, that means *not* providing poor
interfaces. For a library, this is where the responsibility stops, and
clients can do whatever they want--good or bad. They *should* understand
the issues that result from what they do and do accordingly, but that is
still up to them.

In the case of the pp-lib, I'm the primary author and maintainer.
Obviously, it's an open-source, multi-developer project, so others can
make changes, but I will certainly push back against adding interfaces to
the library which I believe to be bad interfaces. For the three
interfaces in question:

0) IS_NULLARY, IS_UNARY, IS_BINARY, ... = good interfaces (< C99 || < C+
+0x)

1) IS_VARIADIC = good interface (>= C99 || >= C++0x)
2) IS_EMPTY = good interface only in a particular domain of applicability
3) IS_TUPLE = bad interface

The IS_TUPLE scenario, even if it wasn't for the serious input
constraints required by any IS_EMPTY implementation, is fundamentally
broken in the larger context. IS_ARRAY and IS_LIST would have even more
constraints. IS_SEQ would have similar constraints. But the root
problem with all of these is the massive ambiguity that results from a
typeless system.

Is (a) a one-element tuple or a one-element sequence? Is (a, b) a two-
element tuple or a one-element variadic sequence? Is (3, (a, b, c)) a
three-element array, a two-element tuple, or a one-element variadic
sequence? These are all questions that cannot be answered without
context (i.e. meaning) that is not present in the structural form of the
input. Further, all of these various forms arise naturally in various
contexts--these aren't corner cases.

Aside from all implementability issues, one could say:

IS_ARRAY ( (3, (a, b, c)) ) // 1
IS_SEQ ( (3, (a, b, c)) ) // 1
IS_TUPLE ( (3, (a, b, c)) ) // 1

IS_ARRAY ( abc ) // 0
IS_SEQ ( abc ) // 0
IS_TUPLE ( abc ) // 0

...but, because of the ambiguity (and, in some cases, lack of
implementability), you cannot dispatch on arrays vs. lists vs. sequences
vs. tuples. Instead, you can only dispatch on "data structure which I am
already expecting" vs. "not a data structure" and that is where
IS_VARIADIC comes in because that already allows the distinction.

For example, let's say you have an interface MACRO(a) where 'a' can be
either a single element of whatever domain you're targeting or it can be
a tuple of elements for whatever domain you're targeting. (For the sake
of argument, let's say that domain is integers.) So, both MACRO(1) and
MACRO((1, 2, 3)) are allowed. What isn't allowed is something like MACRO
((1, 2, 3) 4). If, for the particular domain, that (or something like
it) should be allowed, then that is a different, domain-specific data
structure which is outside the purview of the pp-lib. However, if it
isn't allowed (as is typical), IS_VARIADIC already distinguishes between
the two allowed scenarios and suffers none of the issues associated with
detecting the difference between (1, 2, 3) and (1, 2, 3) 4.

Another example, which *may* be valid in a particular domain, is
initialization of a "real" data structure from variadic data. In that
case, one might have an interface that accepts any of MACRO(1), MACRO(1,
2), MACRO(1, 2, 3), etc.. However, that should be immediately converted
to a real data structure (e.g. tuple or sequence) and subsequently
processed. However, whether that is sound is highly dependent on the
domain of elements. For your case, that domain covers C++ types. There
we already know that we have "open" commas (at least as far as the
preprocessor is concerned), and we already know that variadic data is not
a suitable transmission mechanism for these without encoding. Note that
I'm not implying that there are currently better data structures in the
pp-lib as pp-lib sequences don't allow variadic elements, but that is
where the work, if it's going to be done, should be done. So, as an
example, in the particular case of a list of types, the interface should
be MACRO((T)(U)(V)) for any type T, U, and V implemented on top of
variadic sequence algorithms. A client can then do something like MACRO
(VARIADIC_TO_SEQ(a, b, c)) if they so (unfortunately) choose. Further,
they can define their own MY_MACRO that does this:

#define MY_MACRO(...) MACRO(VARIADIC_TO_SEQ(__VA_ARGS__))

The fundamental issue here is really whether making macros invocations
look like they are not macro invocations is a worthwhile goal as opposed
to an incidental and superficial similarity. IMO, making macro
invocations look like regular function calls--on purpose, rather than
incidentally--is *never*, under *any* circumstances, a worthwhile goal.
That applies to things like the above, but also the even worse scenarios
where people write macros where they bend over backward (e.g. do loop
wrappers) to force a semicolon outside the invocation.

#define MACRO(x) \
  do { \
    /* whatever */ \
  } while (false) \
  /**/

void f() {
   int x = 10;
   MACRO(x);
}

Pure evil. Bad design. Leads to all of the problems that have produced
widespread over-generalizations such as "macros are evil." Macros aren't
evil, the above is evil. As is *any* motivation whatsoever to make a
macro invocation look like the underlying language. The macro
replacement mechanism (really, the preprocessor as a whole) is a
different language whose input is the source code and whose output is the
underlying language. In a sense, it is no different than a DSEL. A DSEL
is a different language embedded in a host language that (typically)
better describes the particular domain than the host language does. Why
is that considered bad in the context of the preprocessor? Why the
constant resistance to treating it as such--especially given the success
of the DSEL concept?

Regards,
Paul Mensonides


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