Mark Pauley
Mark Pauley

Reputation: 1485

How can I guarantee type safety of variadic arguments?

In C, I'd like to make a function or a macro that looks like this:

void Log(char* what, ...) 

where the ... must be key-value pairs of const char*'s. I would really like code that doesn't follow this to blow up at compile time. I tried looking for an __attribute((..)) directive that did this, to no avail. Any ideas?

Upvotes: 3

Views: 506

Answers (4)

mtraceur
mtraceur

Reputation: 3736

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).

Upvotes: 1

Mark Pauley
Mark Pauley

Reputation: 1485

Okay, I figured out a workaround:

#define ASSERT_LIST_TYPE(t, l...) do{t _t[] = {l};}while(0)

void _Log(char* what, ...);

#define Log(what, args...) do{\
  ASSERT_LIST_TYPE(char*, args);\
 _Log(what, args);\
}while(0)

That at least generates a warning if the args aren't of the right type, because the dummy array initialization isn't of the right type.

I plan on #ifdef'ing the ASSERT_LIST_TYPE out if this isn't a debug build just in case..

** Edit **

Based on the feedback in here, I changed the code to look like this:

void _Log(char* what, const char** args, size_t args_len);

#define Log(what, args...) do{\
  const char* a[] = {args};\
  _Log(what, a, (sizeof a)/sizeof(char*));
}while(0)

This appears to work even if args is empty, and it catches somebody passing an obj-c literal string instead (@"..." instead of "...") which is what bit me before.

Upvotes: 1

Ed Swangren
Ed Swangren

Reputation: 124642

where the ... must be key-value pairs of const char*'s

Then you don't want variadic arguments at all. If you know the type(s) you are expecting beforehand then you don't need it and you're just making things more difficult than they need to be.

Just pass in an array.

void Log(const char *what, const char **args, size_t size);

Upvotes: 3

zneak
zneak

Reputation: 138051

C has no way to enforce type safety over arbitrary variadic arguments. GCC and Clang have __attribute__s for some fixed cases (when a variadic function expects the last argument to be NULL, you can use __sentinel__; or when it's a format string, you can use format), but there is no generic solution.

On the other hand, C++11 has variadic templates to solve this problem, but since you mentioned that you're working in C, it's not gonna work for you.

Upvotes: 3

Related Questions