Search code examples
cvariadic-functions

How to call vsprintf multiple times on the same argument list?


I would like a function I can call this way:

    my_printf(int unimportant,
              "%-10s", "a string",
              "%5.1f", 1.23,
              format_string, value_to_format,
              /* possibly more pairs like this */
              (char *) NULL);

We may assume that the format strings contain at most one % escape sequence each.

I would like to use one of the sprintf family to format the values, perhaps producing a result like "a string , 1.2, …".

So far I know several ways that do not work. The following code shows (an incomplete sketch of) the basic idea:

      char * my_printf(int uninmportant, ...)
      {
         va_arg ap;
         va_start(ap, unimportant);
         
         while (1) {
           char *format = va_arg(ap, char *);
           if (!format) break;
           vsprintf(buffer, format, ap);
           /* append buffer somewhere */
         }
       }

This doesn't work because there is no guarantee about the usefulness of ap on return from vsprintf. It could be pointing anywhere or nowhere. The usual fix for that is to copy ap with va_copy before passing it. But that doesn't help in this case:

      char * my_printf(int uninmportant, ...)
      {
         va_arg ap;
         va_start(ap, unimportant);
         
         while (1) {
           char *format = va_arg(ap, char *);
           if (!format) break;
           va_arg ap_copy;
           va_copy(ap_copy, ap);
           vsprintf(buffer, format, ap_copy);
           /* append buffer somewhere */
           /* ?? seek ap forward to point to the next format string ?? */
         }
       }

Now vsprintf no longer destroys ap. But "seek ap forward to point to the next format string" seems quite difficult, because it seems to involve parsing and interpreting the contents of format, at least sufficiently well to know what type to pass to va_arg.

Similarly if I try to get around the problem by using sprintf, I still have to parse the format string in order to uunderstand how to call sprintf:

      char * my_printf(int uninmportant, ...)
      {
         va_arg ap;
         va_start(ap, unimportant);
         
         while (1) {
           char *format = va_arg(ap, char *);
           if (!format) break;
           ??TYPE?? arg = va_arg(ap, ??TYPE??);
           sprintf(buffer, format, arg);
           /* append buffer somewhere */
         }
       }

Again it seems the only way around this is to parse the value of format and then have a big switch with an arm for each possible type.

I can't have been the first or the thousandth person to want to do this. What is the conventional wisdom here?

  1. Here is what you have overlooked: …
  2. You are out of luck, it is well-known that there is no portable solution
  3. The only way to do this is to parse the format strings
  4. There is a dirty trick you can play with the preprocessor as follows: …
  5. The following code, published in 1982 in the Bell System Technical Journal, is helpful, and everyone uses some variation of it: …
  6. Your my_printf calling syntax is misconceived. Have it be called this other way instead: …
  7. (something else?)

I am aware that some compilers, such as GCC, provide nonportable extensions to assist with this, such as parse_printf_format, but I would like a more portable solution.

Thanks for any suggestions.


Solution

  • But "seek ap forward to point to the next format string" seems quite difficult, because it seems to involve parsing and interpreting the contents of format, at least sufficiently well to know what type to pass to va_arg.

    Yes, you cannot definedly advance through the elements of va_list without knowing (closely enough) the type of each element. This is a basic limitation of C variadic functions and the stdarg.h macros.

    it seems the only way around this is to parse the value of format and then have a big switch with an arm for each possible type.

    Yes, that's pretty much right. The job is a bit simplified because

    • the arguments will have been subject to lvalue conversion, which moots all type qualifiers

    • the default argument promotions will have been performed on the argument values, which reduces the number of types you need to consider

    • you need only compatible types, not necessarily exact matches, though in practice, the above points probably moot this.

    • there are a few exceptions to needing even compatible types that might also reduce the number of type you need to consider (see C23 7.16.2.2./2)

    • your problem domain may also limit the number of types you need to consider. For example, the printf family of functions does not support arbitrary pointers, but rather only pointers to char and pointers to void, and those two can be used interchangeably with the stdarg macros. And under some circumstances (related to the actual argument values supported), you might be able to handle unsigned integer types as the corresponding signed integer type, or vice versa.

    Ultimately, however, you need to apply knowledge of the types of the arguments passed in order to advance through the list. More than just argument size, this accounts for the possibility of altogether different argument-passing mechanisms, such as which registers, if any, arguments of different types might be passed in. There is no standard shortcut for this.