3

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.

16
  • 1
    There cannot be a portable solution except for interpreting the format string yourself, because, as you note, ap is indeterminate after passing it to vsprintf, and the C standard does not provide any facility that would tell you how many arguments were consumed in performing the vsprintf other than examining the format yourself. You cannot even write a preprocessing program to transform my_printf calls in some way that supplies the needed information because it would be faced with the same problem, counting the arguments to be consumed, so it has to examine the format strings too,… Commented Dec 11, 2024 at 19:48
  • 1
    Okay, but that is a nearly trivial change. If you can recognize and process a conversion specification, then processing any number of conversion specifications is merely wrapping it in a loop. Commented Dec 11, 2024 at 20:11
  • 1
    You should call va_end() before leaving the function. You also need to call va_end() after you pass ap to vsprintf() because ap is in an indeterminate state on return from vsprintf(). And, therefore, you need to call va_start() again. Each call to va_start() or va_copy() needs a corresponding va_end() in the function that invokes va_start() or va_copy(). Commented Dec 11, 2024 at 20:12
  • 1
    Is there any particular reason you're tied to "single_format, value, single_format, value... " rather than the traditional "full_format, value, value, value, ..."? Commented Dec 11, 2024 at 20:20
  • 1
    @MarkDominus Note that with C23, int unimportant is not needed. Commented Dec 11, 2024 at 22:44

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.