Printf Is Useless
By Artyom Bologov
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:
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
- Is un-ergonomic in the simplest case of outputting a single number;
- Puts data away from its representation;
- And is overcomplicated/unnecessary for non-number output cases.
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:
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
andfputs
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
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.
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 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:
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?