3

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?

tshepang
  • 12,111
  • 21
  • 91
  • 136
Mark Pauley
  • 1,445
  • 12
  • 11
  • 3
    You can't, sorry. Also, don't forget that you'll need a way to signal that there are no more value pairs (with a null sentinel value, for instance). – zneak Jan 22 '14 at 22:48
  • 1
    Better to pass in an array or two arrays instead, there's no real point using variadic functions if you know from the beginning what types you need your arguments to be. – Crowman Jan 22 '14 at 22:49

4 Answers4

3

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.

zneak
  • 134,922
  • 42
  • 253
  • 328
  • Yep, just going to use a NULL-terminated array. I'm already using the format attribute for another version of this, but was asked to add key-value pairs for log metadata. – Mark Pauley Jan 23 '14 at 04:31
3

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);
Ed S.
  • 122,712
  • 22
  • 185
  • 265
  • Interesting, I was really set on using a variadic log function here. I really like your point though, and you may have just solved another problem for me (namely how to do print-f style logging with key-value metadata). – Mark Pauley Jan 23 '14 at 01:56
  • @MarkPauley: For that one, look into the wrappers which take a `valist`. All functions which take variadic arguments should provide one. – Ed S. Jan 23 '14 at 04:07
1

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.

Mark Pauley
  • 1,445
  • 12
  • 11
  • 4
    Careful with that: it will double-evaluate your arguments, once for the array initialization and once when you pass them to the function. If you only pass locals and expressions without side effects, you're fine, but if you straight up pass a function call, you could have nasty problems that will be harder to diagnose than using the wrong type in a variadic function call. – zneak Jan 23 '14 at 02:36
1

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

mtraceur
  • 3,254
  • 24
  • 33