Home > database >  Could you write a printf wrapper that interleaves formatting code and arguments?
Could you write a printf wrapper that interleaves formatting code and arguments?

Time:01-25

I like the succinct format specifiers that printf offers such as %1.3f, as compared to cout, but hate how you have to put all the variables you are going to print at the end of the argument list, which is prone to errors whenever you want to add a new item to be printed. Here's what I'd like to do. Instead of

printf("\n\n%ix%i%c %1.2f\n",sw, sh, interlaced, refresh);

I'd like

printf("\n\n%i", sw, "x%i", sh, "%c ", interlaced, "%1.2f\n", refresh);

It seems like this should be possible with the right wrapper function, but writing variable argument functions is a black art to me. Or has somebody already written this? It seems like such an obvious idea.

CodePudding user response:

Easy:

printf("\n\n");
printf("%i", sw);
printf("x%i", sh);
printf("%c ", interlaced);
printf("%1.2f", refresh);
printf("\n");

CodePudding user response:

For a strictly C answer, yes you can, but people don’t because the problem becomes when do the arguments end?

A normal format specifier string matches specifiers in both number and type to the arguments that follow. When there are no more specifiers, any remaining arguments (should there be any) are simply ignored.

But if you want to interleave, then you must have a terminal argument, such as NULL, in order to signal end-of-arguments. Your interleaved printf would then look like this:

iprintf( "%d", my_int, "%s", "my string", "%f", my_float, NULL );

That last NULL argument is the thorn that people don’t like, in large part because it can be accidentally forgotten, leading to UB code!

Consequently, I will not present a solution for the above syntax.

Evil Macros to the Rescue

Yes, we’re not done. It is entirely possible to create the syntax you like by abusing the C preprocessor! The good news is that this also solves the UB problem with non-terminal argument lists.

The bad news is that it takes a lot of LOC to make it happen: specifically, you must declare your magic function and then override its declaration with macros, which is an evil thing to do, as the macros have no awareness of context and could clobber something totally unrelated.

(It will take me a few minutes to design and test some code for you.)

EDIT: heh, actually, I really don’t want to rewrite printf tonight... (because that is what is needed, basically). In other words, this weird syntax can be done, but no one wants to. I don’t.

Caveats

Both of these solutions have a significant drawback: C compilers have code in them to check that printf-family functions have the correct number and type of arguments to match the format argument.

Neither of the above versions can do that. So if, for example, you mis-match the types of arguments, you are SOL.

CodePudding user response:

In case you are using C , you could potentially do this like:

template<typename ... Args, std::size_t ... N>
void myPrint_impl(std::tuple<Args...> tup, std::index_sequence<N...>)
{
    (std::printf(std::get<N * 2>(tup), std::get<N * 2   1>(tup)), ...);
}

template<typename ... Args>
void myPrint(Args ... args)
{
    return myPrint_impl(std::make_tuple(args...), std::make_index_sequence<sizeof...(args) / 2>{});
}

int main()
{
    myPrint("\n\n%i", 10, "x%i", 20, "%c ", 'o', "%1.2f\n", 1.234);
}

CodePudding user response:

Could you write a printf that interleaves formatting code and arguments? Yes, sort of. But not by writing any kind of a "wrapper" around printf. To do it at all (and due to what you called the "black art" of varargs functions), we would have to write our own version of this "multi printf", from scratch.

Also there's a complication in that it will be hard for multi-printf to know when it's done. Normally, printf reads exactly one format string, and then for each % in the format string, it expects (usually) one more argument. If we want a version that can read a format string, and some arguments, and then another format string, and then some more arguments, how will it know when it's done? After processing one format string, and its arguments, how will our multi-printf know whether or not it should look for an additional format string?

Remember, the rule is that a varargs function must be able to tell, from the arguments it fetches, how many arguments it should expect (and of what type). There's no independent way for it to know how many arguments were actually passed this time.

So the multi-printf I'm going to implement here is slightly different than the one you imagined. It takes any number of format strings, interspersed with arguments, but the last format string must either have no additional arguments (contain no % signs), or it must be a null pointer.

This code is a modification of the miniprintf function from question 15.4 of the C FAQ list. Please refer to question 15.4 and make sure you have a basic grasp of how the miniprintf function there works. (That question and others in section 15 should dispel at least some of the "black magic" concerning varargs functions.) Essentially, I'm taking the format-parsing and argument-interpolating code from miniprintf, and slapping a do/while loop around it so that it can parse an arbitrary number of additional format strings per call.

Here's the code. It requires a "helper" function, baseconv, from question 20.10 of the FAQ list.

#include <stdio.h>
#include <stdarg.h>

void
multiprintf(const char *fmt, ...)
{
    const char *p;
    int i;
    unsigned u;
    char *s;
    va_list argp;

    va_start(argp, fmt);

    do {
        int nperc = 0;

        for(p = fmt; *p != '\0'; p  ) {

            if(*p != '%') {
                putchar(*p);
                continue;
            }

            nperc  ;

            switch(*  p) {
                case 'c':
                    i = va_arg(argp, int);
                    putchar(i);
                    break;

                case 'd':
                    i = va_arg(argp, int);
                    if(i < 0) {
                        i = -i;
                        putchar('-');
                    }
                    fputs(baseconv(i, 10), stdout);
                    break;

                case 'o':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 8), stdout);
                    break;

                case 's':
                    s = va_arg(argp, char *);
                    fputs(s, stdout);
                    break;

                case 'u':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 10), stdout);
                    break;

                case 'x':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 16), stdout);
                    break;
            }
        }

        if(nperc == 0) break;
    
        fmt = va_arg(argp, char *);

    } while(fmt != NULL);

    va_end(argp);
}

And here's a test program to call it:

int main()
{
    int sw = 12;
    int sh = 0;
    int interlaced = 'A';
    multiprintf("%d ", sw, "x%d ", sh, "%c\n", interlaced, NULL);
    char *s = "four";
    multiprintf("int: %d ", sw, "char: %c ", '3', "string: %s", s, "\n");
}

Now, although this code does answer your question, I have to say I have pretty grave doubts that it would really end up being that useful in practice. printf is already hard enough to call correctly. Many programmers have difficulty keeping the number (and especially the types) of the extra arguments right, and this new function only exacerbates those difficulties. It would be hard to remember to keep the last format argument %-free. (It might make sense to define an additional termination condition, namely that it would look for another format string only if the previous one didn't end with \n.)

Also, a few more disclaimers:

  1. Like the FAQ list's miniprintf, the version I've shown is stripped-down and incomplete: it doesn't do floating-point (%e or %f or %g), it doesn't do field widths or precisions, it doesn't return a proper value, etc., etc.
  2. Strictly speaking, if you want to use a null pointer to terminate the format list here, you have to use the syntax (char *)NULL; you can't use plain NULL. (See question 5.11 in the FAQ list for the reason.)

The other possibility to contemplate is, what if you could embed variable names directly in the format string? Going back to your original example, what if you could write it as something like

interpolatingprintf("%{sw:i} x%{sh:i} %{interlaced:c} %{refresh:1.2f}");

The problem, of course, is that this could never work with interpolatingprintf as a "regular" function. This sort of thing would require compiler support, to parse the format string and pick out the variable names so that they could be evaluated and passed. (Also one wonders if things like interpolatingprintf("%{a b:d}") could or should be allowed.)


Finally, for completeness, since Stack Overflow answers are supposed to be standalone, here's the baseconv code from the FAQ list's question 20.10:

char *baseconv(unsigned int num, int base)
{
    static char retbuf[33];
    char *p;

    if(base < 2 || base > 16)
        return NULL;

    p = &retbuf[sizeof(retbuf)-1];
    *p = '\0';

    do {
        *--p = "0123456789abcdef"[num % base];
        num /= base;
    } while(num != 0);

    return p;
}
  •  Tags:  
  • Related