P99
Macro programming with P99

Most macros and features for macro programming with P99 are defined in Meta_programming. This allows operations such as

Argument List Counting
rudimentary argument list processing
to obtain e.g a sublist of the argument list (P99_NARG) or revert an argument (P99_REVS)
Code Unrolling
not restricted to usual for loops but also e.g to produce a sequence of declarations with initializers (P99_VASSIGNS)
constant generation
to compose double constants
type and keyword classification
Scope-bound resource management with for-statements

Argument List Counting

To implement macros in which evaluation depends upon the number of arguments received, we will need to determine how many arguments are received. This can be achieved with something like the following:

#define P00_ARG2(_0, _1, _2, ...) _2
#define NARG2(...) P00_ARG2(__VA_ARGS__, 2, 1, 0)

If NARG2 is called with two arguments the 2 of its expansion is in third position and we will see this 2. If it is called with just one argument the 1 will be in that place and thus be the result of the expansion. You can probably imagine an extension of that macro to treat more arguments, look into the P99 sources to see a version for P99_MAX_NUMBER.

The toy macro NARG2 has an important property, namely that it swallows all its arguments when called correctly (say with 0, 1 or 2 arguments) and just returns a token that corresponds to the number. Such a property is important for macro programming since we don't want to have the compiler itself see the same expressions multiple times.

The NARG2 has a major disadvantage: it is unable to detect an empty argument list. This is due to a fundamental difference between C and its preprocessor. For C, a parenthesis () is empty and contains no argument. For the preprocessor it contains just one argument, and this argument is the empty token  .

So in fact NARG2 cheats. It doesn't count the number of arguments that it receives, but returns the number of commas plus one. In particular, even if it receives an empty argument list it will return 1. The macro P99_NARG deals with that and returns the token 0, if the list is empty.

Other macros are then programmed with similar tricks as are used for NARG2, here: the variable argument list is positioned at the beginning of a new macro list that is then completed by a list of values that contain the different tokens that complete the given list, if necessary.

A second trick is then to paste the name of another macro with that number together. Look e.g at P99_DUPL. When called as follows

(P99_DUPL(3, A)) ==> (P00_DUPL3(A))

this then is replaced 4 as follow

(P00_DUPL3(A)) ==> (A, P00_DUPL2(A)) ==> (A, A, P00_DUPL1(A)) ==> (A, A, A)

The example of P99_DUPL together with ::P00_DUPL1, ... in file p99_generated.h shows a general strategy to overcome the lack of control structures or recursion in the preprocessor. But generally it would be tedious and error prone to have to copy similar definitions over and over again. Therefore P99 implements some of these with a script and collects them in the above mentioned include file. This is probably not something you should do yourself. Section Code Unrolling will show how to avoid this with higher level preprocessor programming constructs.

The next step in macro programming is then to use P99_NARG to obtain the length of a list and to use this numeric token to derive a macro name with the number in it

P99_PASTE(t, o, k, e, n)
==>
P00_PASTE(P00_NARG(t, o, k, e, n), t, o, k, e, n)
==>
P00_PASTE(5, t, o, k, e, n)
==>
P00__PASTE(P99_PASTE, 5, t, o, k, e, n)
==>
P99_PASTE ## 5(t, o, k, e, n)
==>
P99_PASTE5(t, o, k, e, n)
==>
.
==>
token
Footnotes:
4 The actual definition is a bit more complicated to capture special cases.

Code Unrolling

Code unrolling is a generalization of what classicaly would be left to the compiler (and not the preprocessor): loop unrolling. Suppose that in some context you have a loop of fixed length

for (unsigned i = 0; i < 4; ++i)
a[i] = b[i];

There is a good chance that an optimizing compiler would unroll this loop

a[0] = b[0];
a[1] = b[1];
a[2] = b[2];
a[3] = b[3];

This would have the advantage to spare a CPU register otherwise used for i and also that the addressing of the individual elements now can be done with constant offsets from the basepointers of a and b.

We will see below how to achieve such loop unrolling directly with the preprocessor, avoiding reliance on the compiler.

But code unrolling can do more than that. It may be helpful where we have repeated pieces of code for which there is no loop construct in C, for example in a declaration:

signed a[4] = { b[0], b[1], b[2], b[3] };

With P99 you can write this simply as

signed a[4] = { P99_ACCESSORS(b, 4) };

If the length of your vectors might perhaps change during the development of your program, you can have the length of the array as a compile time parameter

#define N 4
.
signed a[N] = { P99_ACCESSORS(b, N) };

You'd later just change N and the rest of your code remains consistent.

As another example of flexibility take another assignment example, namely the "deserialization" of a struct. Suppose that we have a variable of a struct type to which we want to assign values that we receive through an array b:

A.x = b[0];
A.y0 = b[1];
A.o7 = b[2];
A.k = b[3];

Here the pattern is somewhat regular, you assign the elements of b in order, but the left hand sides are arbitrary. P99 can do that for you

P99_VASSIGNS(b, A.x, A.y0, A.o7, A.k);

BTW, with the two P99 macros that we just introduced we can now perform the loop unrolling from the beginning:

The "control structure" that is behind these two macros is called P99_FOR. This is by itself a macro that takes at least 4 parameters, named NAME, N, OP, FUNC, but generally will have more, namely a list A0, A1, ... , A{N-1} of extra arguments. The idea behind this is simple:

  • the argument FUNC is supposed to be a macro name that is going to be applied to each of the A{i}
  • argument OP in turn is a macro that is called to control the "glue" between the different occurrences of the FUNC invocations.

An example of FUNC would be something like NAME[i] for an accessor. For OP we provide simple choices as ::P00_SEQ which just puts commas between the occurrences or ::P00_SEP to do so with semicolons. For more exact examples and the syntax for OP and FUNC please refer to the documentation of P99_FOR.

P99_ACCESSORS
#define P99_ACCESSORS(X, N)
Definition: p99_map.h:199
P99_DUPL
#define P99_DUPL(...)
Construct a list that repeats the argument list N times.
Definition: p99_list.h:90
P99_VASSIGNS
#define P99_VASSIGNS(NAME,...)
Vector-assign to a list.
Definition: p99_map.h:210
i
P00_CLAUSE2 i(_Pragma("weak p00_getopt_comp"))(_Pragma("weak p00_getopt_comp
P99_PASTE5
#define P99_PASTE5(_1, _2, _3, _4, _5)
Definition: p99_paste.h:87
P99_PASTE
#define P99_PASTE(...)
A left-to-right associative paste operator.
Definition: p99_logical.h:357