Type-Safe Variadic Functions in C
(with automatic argument counting)
#define NICER_VARIADIC(type, ...) \
(type[]){__VA_ARGS__}, \
(sizeof((type[]){__VA_ARGS__}) / sizeof(type))
void Log(char * message,
char * * key_value_pairs,
size_t key_value_pair_count);
#define LOG(message, ...) \
Log(message, NICER_VARIADIC(char * *, __VA_ARGS__))
Now let's unpack that a bit.
First, like the other answers already suggested, start by writing your function itself to take an array instead of a variadic argument list (this isn't strictly necessary for this trick, but the trick makes more sense this way, and I think it's better design - it's also usually nicer for interop with other languages and at the ABI level, in my limited experience):
void Log(char * message,
char * * key_value_pairs,
size_t key_value_pair_count);
Second, we need something that will turn an argument list like foo, bar, qux
into a compound literal like (char * * []){foo, bar, qux}
so that we can pass it as an array. Easy enough with variadic macros:
#define NICER_VARIADIC_ARGUMENT_LIST(type, ...) (type[]){__VA_ARGS__}
Now, instead of the compiler comparing foo, bar, qux
against the type-less variadic argument list, it compares it against the typed array literal.
Third, once we've captured the arguments in an array literal, we can use sizeof
to count them (by getting the size of the array literal and dividing it by the size of the type)
#define NICER_VARIADIC_ARGUMENT_COUNT(type, ...) (sizeof((type[]){__VA_ARGS__}) / sizeof(type))
This argument count will be computed at compile-time in any serious compiler because it is a constant expression, and we don't get any multiple-evaluation problems like macros sometimes have, because sizeof
just figures out the type information of its argument without "evaluating" it.
Then we can combine the above two macros into one, which expands to both the array literal argument and the size argument, for better DX:
#define NICER_VARIADIC(type, ...) \
(type[]){__VA_ARGS__}, \
(sizeof((type[]){__VA_ARGS__}) / sizeof(type))
So now you can just do stuff like
LOG("hooray!");
LOG("hooray!", my_key_value_pair);
LOG("hooray!", my_key_value_pair, my_other_k_v_pair);
and it just works, with type-safety (and without boilerplate to pass a separate size or terminating value in the argument list).