I made a function that prints pairs of keys and values, where the keys are trusted compile time literal strings which possibly can contain printf specifiers. Now my question is: is this function legal according to the c standard (like c99)? If not, can I do anything to conform?
void pairs(char *message, ...)
{
va_list args;
va_start(args, message);
char *key = va_arg(args, char *);
if (key == NULL) {
va_end(args);
return;
}
while (key != NULL) {
if (key[0] == '%') {
int len = strlen(key);
printf("%s=", &key[len + 1]);
// Copy the current state of the va_list and pass it to vprintf
va_list args2;
va_copy(args2, args);
vprintf(key, args2);
va_end(args2);
// Consume one argument assuming vprintf consumed one
va_arg(args, char *);
} else {
// if no format assume it is a string
fprintf(_log_stream, "%s=\"%s\"", key, va_arg(args, char *));
}
key = va_arg(args, char *);
if (key == NULL)
break;
printf(", ");
}
va_end(args);
}
It should work something like
pairs("This is a beautiful value",
"%d\0hello", 10,
"pass", user_var,
"hello", "bye", NULL);
And it actually outputs correctly: hello=10, pass="sus", hello="bye"
So it works on my machine (using gcc). But again, is it compliant?
Edit: I found https://www.gnu.org/software/libc/manual/html_node/Variable-Arguments-Output.html
Portability Note: The value of the va_list pointer is undetermined after the call to vprintf, so you must not use va_arg after you call vprintf. Instead, you should call va_end to retire the pointer from service. You can call va_start again and begin fetching the arguments from the start of the variable argument list. (Alternatively, you can use va_copy to make a copy of the va_list pointer before calling vfprintf.) Calling vprintf does not destroy the argument list of your function, merely the particular pointer that you passed to it.
Which is the reason why I use va_copy in the first place, but I am not sure if there is a way to get around that restriction.
No.
No. The:
is undefined behavior. The next argument is
10, which is anint.(Also, technically,
NULLis not achar *.NULLis either anintorvoid *, no one knows. It is also not valid forNULL. But, nowadaysNULLis universally(void *)0, and on sane systems all pointers are the same size, bottom line if you usechar *no one will notice. But if you want to be correct, the argument should be(char *)NULL.)What you want to do in the way you want to do is not possible. You are not able to use
va_listaftervprintfhas used it. Aftervprintfhas usedva_listyou have tova_endit.If you want to do that, you have to parse the format string yourself and get the types of arguments yourself and advance
va_listyourself and either implementvprintfyourself or you could call it separately.Och yea, sure, totally. So let's implement a
pairsas a variadic macro that just forwards toprintf.The call becomes a single
printfwith space joined arguments. The first overloaded macro joins the strings into one string, and then the second overloaded macro takes the second arguments. It is a simple shuffle to re-order arguments forprintf.But this is not satisfactory. You might delve into this more, by using
_Genericto guess the format specifier. From the arguments, you can construct an array of "printers" that takeva_listby a pointer. Not by value! You can only modify the "upper" functionva_listby passing with a pointer. Then you can iterate over printers to print the values, one by one.Now you can expand this method, by for example including in the
"hello"a format specifier like you wanted, and then extract it. For example"%d\0hello", if the string starts with%, split it on\0and use the leading part for printing inside the printer. I think this is the interface you aimed for.Or you can expand such a library and start supporting python format strings and so on, which is what I did with now dead yio library, as I do not have time for it.