____ _ | __ )| | ___ __ _ | _ \| |/ _ \ / _` | | |_) | | (_) | (_| | |____/|_|\___/ \__, | |___/_ _ _/_| |__ ___ _ __| |_ _ _/_ / _ \ '_ \ / _ \ '__| | | | |/ _ \ __/ |_) | __/ | | | |_| | __/ \___|_.__/ \___|_| |_|\__,_|\___|
[2026-10-27][C->Macros]
I'm not sure if the macro I've written is ugly or elegant:
#define CALL_TEST(...) do { \
void *call_test_args_ __VA_OPT__([]) = \
__VA_OPT__({) NULL __VA_OPT__(, __VA_ARGS__ }); \
size_t nargs = \
__VA_OPT__(sizeof call_test_args_ / sizeof call_test_args_[) 0 __VA_OPT__(] - 1); \
test(call_test_args_ __VA_OPT__(+ 1), nargs); \
} while (0)
But to understand how I've come up here, let's discuss about the initial problem. I am currently re-writing DAHL into Auto-DAHL, a better version. I have a function that will be called a lot of times and that looks like this:
void dahl_invoke(
void* buffers[], void* cl_arg,
size_t nbuffers, size_t granularity_level,
struct starpu_codelet* cl)
# Then it is invoked as:
void* buffers[3] = {
image_samples.children[sample_ind], filters, output
};
dahl_invoke(buffers, NULL, 3, 1, &my_cl_convolution_filters);
# Or using a one-liner:
dahl_invoke(
(void*[3]) { image_samples.children[sample_ind], filters, output },
NULL, 3, 1, &my_cl_convolution_filters);
Having to always create a temporary stack allocated array to pass all the buffers is not very practical, and we could clearly guess the number of buffers at compile time. My first guess looked like this (for simplicity we ignore every other parameters of dahl_invoke
#define CALL_TEST(...) \
dahl_invoke((void*[]) { __VA_ARGS__ }, sizeof((void*[]) { __VA_ARGS__ }) / sizeof(void*))
CALL_TEST(ap, bp, ap);
// Would expand to:
dahl_invoke((void *[]){ap, bp, ap},
sizeof((void *[]){ap, bp, ap}) / sizeof(void *))
CALL_TEST();
// Would expand to:
dahl_invoke((void *[]){}, sizeof((void *[]){}) / sizeof(void *))
Basically we create the array on the fly given the variadic arguments, and we count the number of
arguments by creating it again, computing the size in bytes, then dividing it by the size of
void* to get the number of elements. While this works, it still generates a warning,
which would end up everywhere the macro is written:
va_args.c:25:5: warning: ‘dahl_invoke’ accessing 8 bytes in a region of size 0 [-Wstringop-overflow=]
25 | dahl_invoke((void*[]) { __VA_ARGS__ }, sizeof((void*[]) { __VA_ARGS__ }) / sizeof(void*))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A cleaner refactor is to enclose everything in a do { } while(0), so that we can
write multiple statements and add a temporary variable for the array:
#define CALL_TEST(...) do { \
void *args[] = { __VA_ARGS__ }; \
dahl_invoke(args, sizeof(args) / sizeof(args[0])); \
} while (0)
For the warning, one idea was to force the array being non-empty using a NULL as the
first element, but passing the pointer of the array from the second element:
#define CALL_TEST(...) do { \
void *args[] = { NULL , __VA_ARGS__ }; \
dahl_invoke(args + 1, sizeof(args) / sizeof(args[0]) - 1); \
} while (0)
However, it seems that GCC is still able to catch this and emits a warning! It knows that
args + 1 is a empty array when the macro is called without variadic arguments (we can
verify that by removing the + 1, and it actually removes the warning, but of course calls with
arguments don't work properly anymore).
At this point, I remembered about __VA_OPT__(), which basically only expands if
__VA_ARGS__ is not empty. I also remembered that in the original call, when I wanted to
pass empty arguments I would just do:
dahl_invoke(NULL, 0);
Basically, we just want to pass NULL and 0 when we have no buffers. And it turns
out that, we exactly have these tokens at our disposition in the previous macro: NULL
as the first element of the array, and [0] when selecting the first arguments.
The solution with __VA_OPT__() becomes trivial. We just need to get rid of
parentheses in sizeof calls, which luckily can be written without parentheses.
Otherwise it messes with __VA_OPT__() ones (e.g.:
__VA_OPT__( sizeof ( args[0 ) ]) ).
In the end, we obtain what I shown earlier:
#define CALL_TEST(...) do { \
void *ARGS_ __VA_OPT__([]) = \
__VA_OPT__({) NULL __VA_OPT__(, __VA_ARGS__ }); \
size_t NARGS_ = \
__VA_OPT__(sizeof ARGS_ / sizeof ARGS_[) 0 __VA_OPT__(] - 1); \
dahl_invoke(ARGS_ __VA_OPT__(+ 1), NARGS_); \
} while (0)
Calling the macro with multiple arguments (CALL_TEST(ap, bp, ap)) would expand to:
do {
void *ARGS_[] = { NULL, ap, bp, ap};
size_t NARGS_ = sizeof ARGS_ / sizeof ARGS_[0] - 1;
dahl_invoke(ARGS_ + 1, NARGS_);
} while (0)
Calling the macro without arguments (CALL_TEST()) would expand to:
do {
void *ARGS_ = NULL;
size_t NARGS_ = 0;
dahl_invoke(ARGS_, NARGS_);
} while (0)
Let's detail each line expansion:
void *ARGS_ __VA_OPT__([]) = \
__VA_OPT__({) NULL __VA_OPT__(, __VA_ARGS__ }); \
Here, when no arguments it removes all elements defining the array: [],
{, and , }.
size_t NARGS_ = \
__VA_OPT__(sizeof ARGS_ / sizeof ARGS_[) 0 __VA_OPT__(] - 1); \
Here, the expansion removes almost everything but the 0 that was initially used to access
elements 0 of the array: sizeof ARGS_ / sizeof ARGS_[ and ] - 1.
The whole source can be found here, and compiled with gcc -std=c23
-Wall -o va_args va_args.c && ./va_args.
So that's it. Ugly or elegant?