Jens Gustedt's Blog

March 18, 2011

Statement unrolling with P99_FOR

Filed under: P99, syntax — Jens Gustedt @ 23:00

Specifying code that does same or equivalent things to a list of variables, tokens, items or so is an important source of annoyance. It can easily make code unreadable and difficult to maintain. E.g consider the following

if ((b == a) && (c == a) && (d == a) && (e == a) && (f == a)) {
  /* all are equal, do something special */
} else {
  /* handle the base case */

Understanding what that expression does is difficult in one glance and updating it is error prone. Adding another variable, say g, to be tested may be simple but withdrawing a is a major operation.

With code unrolling as P99 provides it we may implement a macro P99_ALLEQ

if (P99_ALLEQ(a, b, c, d, e, f)) {
  /* all are equal, do something special */
} else {
  /* handle the base case */

Here, adding another variable to the list or deleting any of them is not a big deal.

In fact, the implementation of such a macro with P99 is as simple as this:

#define P00_ISIT(WHAT, X, I) (X) WHAT
#define P99_ALLEQ(FIRST, ...) P99_FOR(== (FIRST), P99_NARG(__VA_ARGS__), P00_AND, P00_ISIT, __VA_ARGS__)

This uses P99_FOR to do a macro iteration. That iterative evaluation will produce code for each element that is passed in the __VA_ARGS__ list, `b, c, d, e, f‘ in our example.

Each of these elements is given to the P00_ISIT macro as a second argument, the first being the first argument to P99_FOR. Here that first argument is == (FIRST), in our example this leads to `== (a)‘. So when processing the b in the list that leads to `(b) == (a)‘, for c that is `(c) == (a)‘ and so on.

The other macro that is in the arguments of P99_FOR is P00_AND. As you expect this is what is responsible for the && operators between these individual expansions for each list item. In fact it does a bit more than given above in that it adds a lot more parenthesis. The real expansion done by the preprocessor is

if ((((((((((b) == (a)) && ((c) == (a)))) && ((d) == (a)))) && ((e) == (a)))) && ((f) == (a)))) {
  /* all are equal, do something special */
} else {
  /* handle the base case */

After we withdraw a from the list of arguments the new macro expansion would read

if ((((((((c) == (b)) && ((d) == (b)))) && ((e) == (b)))) && ((f) == (b)))) {
  /* all are equal, do something special */
} else {
  /* handle the base case */

In the same way as P99_ALLEQ we may define a whole bunch of other macros that work in the same spirit.

#define P99_MINOF(FIRST, ...) P99_FOR(>= (FIRST), P99_NARG(__VA_ARGS__), P00_AND, P00_ISIT, __VA_ARGS__)
#define P99_INFOF(FIRST, ...) P99_FOR(> (FIRST), P99_NARG(__VA_ARGS__), P00_AND, P00_ISIT, __VA_ARGS__)
#define P99_MAXOF(FIRST, ...) P99_FOR(<= (FIRST), P99_NARG(__VA_ARGS__), P00_AND, P00_ISIT, __VA_ARGS__)
#define P99_SUPOF(FIRST, ...) P99_FOR(< (FIRST), P99_NARG(__VA_ARGS__), P00_AND, P00_ISIT, __VA_ARGS__)
#define P99_ONEOF(FIRST, ...) P99_FOR(== (FIRST), P99_NARG(__VA_ARGS__), P00_OR, P00_ISIT, __VA_ARGS__)

Here the first use the same macro P00_AND as before, only P99_ONEOF, the last, uses P00_OR to combine the different parts.

P99_FOR can also be used to build expressions that are a more complicated.

P99_ORDERED(<=, a, b, c, d) 

should expand to

((a) <= (b)) && ((b) <= (c)) && ((c) <= (d))

Here you see that in the expanded expression b and c each appear two times. So a simple generalization of the tricks above would not work. We have to go in steps to generate that. First, we define a macro that expands to the inner part of the expression:

(b)) && ((b) <= (c)) && ((c)

You might already see the pattern. We have parts per list item that look similar to `(b)) && ((b)‘ and the separation is done with the operator `<=‘. This is done by these two macros:

#define P00_ORDERED_AND(_0, X, _2) (X)) && ((X)
#define P00_ORDERED_OP(OP, _1, X, Y) X OP Y

Observe the unbalanced parentheses in P00_ORDERED_AND. These two are combined in

#define P00_ORDERED_MID(OP, N, ...)                                        \

it supposes that it obtains the desired length N as second argument. It also uses another generic P99 macro, P99_SUB, to compute the sublist of the arguments that starts at position 1 and has length N. E.g P00_ORDERED_MID(<=, 2, r, s, t) will expand to

(s)) && ((s) <= (t)) && ((t)

Now this is not yet a valid C expression, only the middle part of what we want, but the following cascade of four defines does the trick.

#define P99_ORDERED(OP, ...) P00_ORDERED(OP, P99_NARG(__VA_ARGS__), __VA_ARGS__)
#define P00_ORDERED(OP, N, ...)                 \
P99_IF_LT(N, 3)                                 \
(P00_ORDERED2(OP,__VA_ARGS__))                  \
(P00_ORDERED3(OP, P99_PRED(N), __VA_ARGS__))
#define P00_ORDERED2(OP, X, Y) (X) OP (Y)
#define P00_ORDERED3(OP, N, ...)                        \
((P99_SUB(0, 1, __VA_ARGS__))                           \
 OP P00_ORDERED_MID(OP, P99_PRED(N), __VA_ARGS__)       \
 OP (P99_SUB(N, 1, __VA_ARGS__)))

First P99_IF_LT is used to find if the number of arguments is less than 3. In the case it is less a special case P00_ORDERED_2 is chosen, namely the one that need no middle part. If there are more arguments macro P00_ORDERED3 prefixes the middle part with the first element of the list and the operator, and appends the analog for the last element. The result of

P99_ORDERED(<=, a, b, c, d)
P99_ORDERED(>, 4, 5)

are what we’d expect:

((a) <= (b)) && ((b) <= (c)) && ((c) <= (d))
(4) > (5)

Blog at