Printf Is Useless

By Artyom Bologov

IMAGE_ALT

A story that actually motivated this post

I worked on extending a C++ tool. Making it produce results with higher precision, in particular. I discovered that it's surprisingly hard to output the custom precision number in C++. Here are two ways I found:

cout.precision(18);
cout << setprecision(18) << "rest of the output";
Changing the precision of stream output in C++

Both of these mean modifying the global state of the stream. Which, in case of cout, is quite a consequential change. Should be avoided at all costs.

The solution my friend (an experienced C++ dev) suggested was to just use printf. But... Is printf the C++ way?

Anyway, printing floats is a mess. Unlike printing anything else. So this post goes into why you don't need printf and the like... unless you, like me, is stuck trying to print floats.

We (programmers) all use formatted output daily. But most formatting primitives, especially in low-level languages, are for numbers. So formatted output

So what I'm trying to say here is not that format strings are useless. I'm rather of the opinion they can be shortened, optimized, and simplified. Except for float printing cases where printing parameters actually matter. The rest is easily accomplished with string interpolation or even separate printing expressions. No meter-thick historic crust.

Scheme and SRFI 48 format

Let's start with something simple. There were several attempts to standardize format strings in Scheme. One is SRFI 28 Basic Format Strings, then SRFI 48 Intermediate Format Strings, and SRFI 54 (cat) for alternative take, and others.

SRFI 28 is quite basic with only three directives:

~a
for human-readable output.
~s
for machine-readable output.
and ~%
for newline.

It's more or less clear why one would want to extend this—while human-readable vs. machine-readable is quite enough for many cases, one might need finer output. That's where SRFI 48 comes in:

This SRFI extends SRFI 28 in being more generally useful but is less general than advanced format strings in that it does not allow, aside from ~F, for controlled positioning of text within fields.
Abstract of SRFI 48, highlighting mine

In other words: SRFI 48 exists for the sole purpose of printing floats prettily. (I mean, there are lots of other directives, mimicking for C printf and Common Lisp format. (Ooops, spoilers!) But these are still mostly falling into the human/machine-readable category.) Nothing new in SRFI 48, except float printing. All the rest is covered by SRFI 28, really.

printf In C: Exists For Floats

C printf is powerful, just enough to power a game of Tic-Tac-Toe. It has lots of modifier flags for its directives:

-
Left-justify the printed value.
+
Always show the sign.
space
Pad the value with spaces.
0
Pad the value with zeros.
#
Alternative form.

And there's a moderate number of directives to choose from. Here's my categorization:

% and n
Trivial/dangerous.
c and s
Have dedicated functions (fputc and fputs respectively) matching them.
p
Somewhat special, but can also be hexadecimal or whatever integer printing by default.
d/i, u, o, x
For integers in different bases.
f, e, a, and g
For floating point numbers.

It's useless, because you have an assortment of number-printing directives. Some of which you're unlikely to ever use. And the useful stuff like string printing is so stripped down that it's almost useless too. How often does C programmer implements conditional printing with

printf("%s%s", (boolean ? "!" : ""), "string");
Conditional printing, C interpretation

Too often. String interpolation akin to JavaScript would make more sense. It would both put the data into the output. And won't bother with the obvious (from the type system point of view) details of printing directives.

console.log(`${boolean ? "!" : ""}string`)
JavaScript interpolated version

While this example would require type inference to be ported to C, it's a good things to aim for. (And you'll see that it's achievable in the end of this post.)

Counterexample: Common Lisp format

Common Lisp's format is likely Turing-complete. Not because of some %n hack that makes printf dangerous. But rather because it was intended as a one-stop shop for output.

I'll refer you to my earlier post for a full feature listing (and horrible format string from my own experience!) But the gist is that format is a control flow of its own, replacing conditionals and loops by format strings. And that makes it more useful, but the problem of increasing formatting-data distance is still there.

Solution: Generic Printing and Interpolation

I'll handle the hardest case out of these three: C data formatting. Thanks to C11 generics, it's possible to implement generic printing (and that's exactly what I'm doing in my Pretty.C) that covers 99% of the printing cases:

 #if __STDC_VERSION__ >= 201112L
 #define print(...)
        _Generic((__VA_ARGS__),
                 _Bool: fputs(stdout, (_Bool)(__VA_ARGS__) ? "true" : "false"),
                 default: printf(
                         _Generic((__VA_ARGS__),
                                  char*: "%s",
                                  char: "%c",
                                  signed char: "%hhi",
                                  short: "%hi",
                                  int: "%i",
                                  long: "%li",
                                  long long: "%lli",
                                  unsigned char: "%hhu",
                                  unsigned short: "%hi",
                                  unsigned int: "%u",
                                  unsigned long: "%lu",
                                  unsigned long long: "%llu",
                                  float: "%g",
                                  double: "%g",
                                  long double: "%Lg",
                                  default: "%p"),
                         (__VA_ARGS__)))
 #endif
Generic printing in C

If there is tgmath.h in C, why is there no tgio.h? I dunno, maybe I should file a proposal to C standard people?

Here's how the example from earlier looks with this new macro:

if (boolean)
        print("!");
print("string");
Problematic example fixed with newly defined print() macro

It's the same length as printf (or even less if we don't consider 8 spaces of indentation!) But the structure and syntax are easier on both human and machine readers.

The solution in Scheme and CL was already proposed: just use language macro facilities to implement printing that actually reflect the output shape. Something like string interpolation, but even more structured and enforceable!

So yes, you likely don't need printf, you need structured and simple output. Like string interpolation or generic printing. You're still stuck with printf if you're on C compilers nostalgic for the times before C11. But, that aside, use some structured output instead of formatted output, won't you?

Leave feedback! (via email)