From the putc
man page, "putc()
is equivalent to fputc()
except that it may be implemented as a macro which evaluates stream more than once."
I wasn't sure what this meant so I asked Shevek.
He launched into a lambda calculus explanation that eventually boiled down to: putc
may evaluate it's arguments more than once, but fputc
evaluates them exactly once.
This is because putc
is implemented as a macro and the C preprocessor just does string substitution, leading to examples like the ones below.
Ignoring what putc
and fputc
actually do, let's look at a simplified version where they both simply do one operation twice and another operation once:
#define putc(v, s) s, s, v
void fputc(v, s) { s, s, v }
putc('a', streams[idx++])
-> streams[idx++], streams[idx++], 'a'
-> idx += 2
fputc('a', streams[idx++])
-> v = 'a'
-> s = streams[idx++]
-> s, s, v
-> idx += 1
The outcome of this is that idx
gets incremented twice by putc
, but only once by fputc
.
That happens because the putc
being a macro means that it cannot evaluate it's arguments before it is "called", it must simply evaluate them at each place where they're used at runtime.
fputc
, on the other hand, evaluates it's arguments once and binds those values to variables inside it's scope.
A more realistic example is one that could actually be a definition of putc
:
#define putc(v, s) do {
s.buf[s.ptr++] = v;
if (s.ptr == BUFSIZ) { fflush(s); }
} while(0)
FILE **streams[100];
void writeall(char c) {
int idx = 0;
while (idx < 100)
putc(c, streams[idx++]);
}
struct FILE {
int ptr = 0;
char buf[BUFSIZ];
}
The while loop was supposed to write a character to each of a hundred streams.
But instead, it wrote a character to every third stream (and when ptr
reaches BUFSIZE, it skips one), because the macro evaluates to:
while (idx < 100) {
streams[idx++].buf[streams[idx++].ptr++] = v;
if (streams[idx++].ptr == BUFSIZE) { fflush(streams[idx++]); }
}
If we had used fputc
instead of putc
, we would have written to each of the 100 streams.
The lambda calculus part comes in because it's the theory that describes this effect.
Suppose we have a function f(x) = x + x
.
Now suppose that x = a + b
.
Because addition is associative, we can evaluate these in any order: we can evaluate x
first and then add it to itself, or we can substitute a + b
into f(a + b) = a + b + a + b
.
We can think of this as a tree:
. answer / \ x + x / \ / \ a + b + a + b
The two ways of evaluating it are just evaluating it from the root down or from the leaves up.
Now in lambda calculus, there are no side effects, so either way works fine.
But if there are side effects of an operation, such as incrementing an index, then the order matters.
putc
is like evaluating from the leaves up, but fputc
is like evaluating from the root down.