Variadic Macros, Token Pasting, and Callback-Style Macro Expansion

C/C++

Posted by Bruce Lee on 2024-05-10

About Me

Welcome to my blog! This is where I collect my observations and notes on programming and technology. The main subjects range from implementation details to broader ideas about programming.

Main Topics

  • Engineering Projects: Exploring implementation details and how technical systems work.
  • C/C++: Notes on language features and programming techniques.
  • The Programmer’s Perspective: Ideas about developing a career and a way of thinking as a programmer.

For more, visit the categories page.

Contact

If you have questions or would like to discuss something, please get in touch through the About page.

Thank you for reading and for your support. I hope these notes help you on your own technical journey!


Variadic Macros, Token Pasting, and Callback-Style Macro Expansion

Variadic Macros Without Token Pasting

The variadic mechanism discussed here is the one used in macro definitions, not the functions and types declared in stdarg.h.

In many cases, __VA_ARGS__ can be used without a preceding ##. For background on the token-pasting operator, see C Preprocessor Token-Pasting Operator.

__VA_ARGS__ is a preprocessor placeholder. During macro expansion, occurrences in the replacement list are replaced with the variable arguments supplied to the macro. Here is a typical use:

1
2
3
4
5
6
7
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("File: %s, Function: %s, Line: %d\n", __FILE__, __func__, __LINE__);
return 0;
}

After preprocessing:

1
2
3
4
5
6
7
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("File: %s, Function: %s, Line: %d\n", "test3.c", __func__, 6);
return 0;
}

The result is exactly what we expect from this familiar pattern. But suppose MESSAGE_OUTPUT is used not only to report its call site, but also to print a simple message:

1
MESSAGE_OUTPUT("This function does some things");

The complete program becomes:

1
2
3
4
5
6
7
8
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("This function does some things");
return 0;
}

Compilation reports:

1
2
3
4
5
6
7
test3.c: In function ‘main’:
test3.c:2:57: error: expected expression before ‘)’ token
2 | #define MESSAGE_OUTPUT(msg, ...) printf(msg, __VA_ARGS__)
| ^
test3.c:7:9: note: in expansion of macro ‘MESSAGE_OUTPUT’
5 | MESSAGE_OUTPUT("This function does some things");
|

This call supplies msg but no additional variable arguments. The preprocessor therefore replaces __VA_ARGS__ with an empty sequence. Inspect the preprocessed file:

1
2
3
4
5
6
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("This function does some things", );
return 0;
}

The problem is now obvious: an extra comma remains in the printf call. That is why the diagnostic says an expression is missing before ).

A variadic macro written this way can fail when invoked without variable arguments. In many ordinary uses, the macro’s intended purpose means callers do supply arguments after the fixed parameter, which can hide this case.

Adding the Token-Pasting Operator

Change the program to:

1
2
3
4
5
6
7
#include <stdio.h>
#define MESSAGE_OUTPUT(msg, ...) printf(msg, ##__VA_ARGS__)
int main(int argc, char** argv)
{
MESSAGE_OUTPUT("This function does some things");
return 0;
}

Only the previously failing case is shown; calls with additional arguments behave as before.

After preprocessing:

1
2
3
4
5
6
# 4 "test3.c"
int main(int argc, char** argv)
{
printf("This function does some things");
return 0;
}

The trailing comma in the printf call has disappeared. In this GNU preprocessor extension, when the variable argument is omitted, , ##__VA_ARGS__ removes the preceding comma. This avoids the error shown above.

The key point is that ## and __VA_ARGS__ belong in macro replacement lists; they are preprocessor facilities, not ordinary C expression syntax.

Passing Code Blocks: A Callback-Like Use of Macros

Variable arguments are not limited to values. They can also contain code to expand at a chosen point inside another macro. This resembles a callback in purpose, though the mechanism is quite different from calling a function.

I encountered this pattern in a project that used many macro wrappers to simplify control flow and separate pieces of functionality. Instead of a function callback, a variadic macro accepted another macro invocation as a code block.

I cannot reproduce the original project here as a self-contained example, so I wrote this small demonstration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#define PLUS(x, y) (x = ((x) + (y)));
#define SUB(x, y) (x = ((x) - (y)));
#define DO_SOMETHINGS(msg, ...) do{ \
printf(msg); \
/*do more things*/ \
__VA_ARGS__ \
} while(0)
int x = 5;
int y = 3;
int main(int argc, char** argv)
{
DO_SOMETHINGS("Do Plus\n", PLUS(x, y));
printf("x = %d, y = %d\n", x, y);
DO_SOMETHINGS("Do Sub\n", SUB(x, y));
printf("x = %d, y = %d\n", x, y);
return 0;
}

DO_SOMETHINGS can perform the supplied operation by accepting an invocation such as PLUS(x, y). Writing DO_SOMETHINGS("Do Plus\n", PLUS); would not do the same thing. A macro name is not a C function address that can be passed around and called in the usual way.

The do { ... } while (0) idiom packages the expansion as a single statement. Braces can also group statements into a block, though they do not provide all the same statement-level behavior.

In DO_SOMETHINGS, the __VA_ARGS__ placeholder appears exactly where the supplied macro invocation should expand and execute.

The compiled program performs the intended operations, but examining the preprocessed source makes the mechanism clearer:

1
2
3
4
5
6
7
8
9
10
11
# 9 "test4.c"
int x = 5;
int y = 3;
int main(int argc, char** argv)
{
do{ printf("Do Plus\n"); (x = ((x) + (y))); } while(0);
printf("x = %d, y = %d\n", x, y);
do{ printf("Do Sub\n"); (x = ((x) - (y))); } while(0);
printf("x = %d, y = %d\n", x, y);
return 0;
}

This test4.i output is the code we wanted: one expanded block performs addition, and the other performs subtraction. The same technique can be used to build useful macro-based abstractions.


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !