Functional Style Preprocessor Macros

The C preprocessor has a deservedly poor reputation as a means to define function-like code. However there aren’t many guaranteed alternatives for trivial code generation without resorting to external tools. I’ve been making use of it for some time to iterate over lists of types when manually instantiating templates, defining enum-to-string conversions, and some trivial (but tedious) free functions. Implementing a map function over lists of tokens using only the preprocessor is a neat way to simplify this whole process.

Outline

In headers throughout my projects I have a few instances of code similar to the below:

#define MAP_LEVEL(F) MAP(F, ERROR, WARN, INFO, DEBUG)

Here we define a macro which takes some user defined macro that will transform one argument. When it is called it applies the provided function F sequentially to each of the predefined arguments resulting in a sequence of code of the form: F(ERROR) F(WARN) F(INFO) F(DEBUG).

It is important that we don’t have to manually consider the argument count here, and that the code is trivially extensible. We are, after all, attempting to reduce the fragility of the code overall.

Approach

Unfortunately I can’t take credit for the broad outlines of the approach below, which I found on stackoverflow. The fundamental insight for this approach is that we can use the parameters provided to MAP as a way of offsetting a list of macros from which we select the nth.

#define DISPATCH(       \
    _01, _02, _03, _04, \
    NAME, ...           \
) NAME


#define MAP(FUNC,...)       \
DISPATCH(__VA_ARGS__,       \
    MAP4, MAP3, MAP2, MAP1  \
)(FUNC,__VA_ARGS__)


#define MAP1(F,X) F(X)
#define MAP2(F,X,...) F(X)MAP1(F,__VA_ARGS__)
#define MAP3(F,X,...) F(X)MAP2(F,__VA_ARGS__)
#define MAP4(F,X,...) F(X)MAP3(F,__VA_ARGS__)

DISPATCH is a function which selects the (statically defined) nth argument supplied to it. Note that the arguments names within DISPATCH are simply placeholders. The trailing ellipsis is used to capture an arbitrary quantity of unused arguments which we may be passing in via the MAP macro which we define next.

The MAP macro selects from a list of predefined macros which take a fixed number of arguments. By ordering the list of concrete implementations we pass to DISPATCH in descending arity we can use __VA_ARGS__ as a method of offsetting the list of candidates functions by n which matches the parameter count of MAP.

It is unfortunate that this approach requires the developer to predefine a sequence of macros that correspond to the maximum arity we expect to take. However, it is primarily a mechanical issue rather than a technical one and is amenable to generation from within your buildsystem.

Usage

There are a number of nifty use cases that immediately spring to mind. Any time we may be repetitively defining small code fragments may be useful.

#include "./preprocessor.hpp"

#define MAP_LEVEL(F) MAP(F, ERROR, WARN, INFO, DEBUG)

#define ADD_COMMA(X) X,

enum class level {
    MAP_LEVEL(ADD_COMMA)
};


constexpr const char*
to_string (level l)
{
    switch (l) {
    #define CASE(L) case level::L : return #L;
    MAP_LEVEL(CASE)
    #undef CASE
    }

    ::debug::unhandled (l);
}

In the above, the enum level for our logging facility is defined in terms of the macro MAP_LEVEL, and the to_string function uses a simple fragment CASE to generate each of the string values. This reduces scope for simple coding mistakes and allows for trivial addition of values to the enum.

Drawbacks

Unfortunately, there are still obvious drawbacks.

You’re still dealing with the C preprocessor, with all of the dangers it exposes. If you have a task that is amenable to external tooling, or you have sufficient time, that may be a more robust approach.

Scaling the argument count tends to interact poorly with at least one IDE. I have used the above approach for MAP of arity 96 without issue, but expanding to over 300 (the number of structures found within the Vulkan specification with extensions) presents an immense cost to my editor; to the point that it is impractical to touch one or two files which use this approach heavily.