One thing that stands out to me is that he uses for loops quite a bit where, to me anyway, a while loop seems more natural.
That part is already discussed in chapter 3.5 Loops While and For on p. 56 of the very first edition of The C Programming Language:
We have already encountered the while
and for
loops. In
while (expression)
statement
the expression is evaluated. If it is non-zero, statement is executed and expression is re-evaluated. This cycle continues until expression becomes zero, at which point execution resumes after statement.
The for
statement
for (expr1 ; expr2 ; expr3)
statement
is equivalent to
expr1;
while (expr2) {
statement
expr3;
}
Grammatically, the three components of a for
are expressions. Most commonly, expr1 and expr3 are assignments or function calls and expr2 is a relational expression.
As to your question:
So here's the retrocomputing part: Plauger was writing in 1992. Is there a specific reason that compilers at the time did better with for loops rather than while loops, or is this just a personal preference of Plauger?
Yes, it's only a matter of style as K&R notes on p. 57 of their book with:
Whether to use while
or for
is largely a matter of taste.
For example, in
while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
; /* skip white space characters */
there is no initialization or re-initialization, so the while
seems most natural.
The for
is clearly superior when there is a simple initialization and re-initialization(*1), since it keeps the loop control statements close together and visible at the top of the loop. This is most obvious in
for (i = 0; i < N; i++)
which is the C idiom for processing the first N elements of an array, the analog of the Fortran or PL/l DO loop.
So Plauger might simply be used to a preference of for
loops shown by the base material and many many examples derived from it.
Take Away #1: Code writing is always a matter of style preference.
The book goes on a bit about further savings in source lines, showing a clear preference of K&R for writing tight source code, almost as readable as APL (but... see below). Of course there could be other PoV.
The discussion about the merits of either version is already there in the very first book about C, more than a decade before Plauger's writing.
Take Away #2: Reading the basics first is a great idea before switching to secondary literature :))
But there's also the but-part about some compiler shortfall: Some of these compacting line saving techniques were also hints for the compiler - or better ways to make less 'intelligent' compilers generate the best code. A line like *s1++ = *s2++
can be turned into an assignment with auto increment operations without much lookahead or reshuffling. Even better, on a CPU which sets the flags according to the last item moved, it spares an additional test for a trailing zero byte (*2).
So even the dumbest compiler, used for a CPU with auto increment and flags set by move will choose the best possible code - and that's what a PDP-11 was. It's one of the artefacts showing that while C may be seen as CPU independent, it was in all practical use constructed to best support a CPU with certain features.
This kind of compiler support became useless if not superficial the moment C was ported to different CPUs (*3) as well with more sophisticated compilers.
Take Away #3: There is a part of compiler hacking included in using such constructs. Even though it was already way outdated when Plauger wrote his book.
A different POV about Style, readability and meaning.
In the mind of K&R for
is kind of a combination of COBOL and APL: the readability of COBOL by putting all items necessary for (basic) loops into a defined structure, so no need to look around for initialization, while keeping it compact like some APL code.
But when looking at it in a more fundamental way I would consider both implementations presented in the question as bad implementations and note it in any code review.
To start with, using while
with anything but a test expression is using side effects. By definition (check K&R) while
is a repeating loop, one that is executed ZERO or more times. No execution of whatever it contains will ever happen if the condition is not true at the beginning of each iteration. Using it as shown goes against that basic principle.
After all, the task of copying a (zero) delimited string needs to transfer at least the trailing delimiter. So the task is to transfer 1..n elements, not 0..n.
Similarly, for
does not fully fit the task, at least not as used. for
defines an iteration as initialization, condition and reinitialization. Again none of the three should be manipulating anything outside of loop control. Except, when taking the items apart, using a for
no longer satisfies the need for a 1..n loop - at least not without a lot of temporary variables/constructs.
The grammatically correct construct to describe this is a do/until as in do/while, so it should look rather like this:
do
st = *s1++ = *s2++;
while (st != '\0');
Yes, I know, unfamiliar to classic C programmers drilled to save on lines at all cost.
*1 - Note the word re-initialization got changed to increment in the second edition.
*2 - This is BTW why Plauger writes (*s++ = *s2++)!= '\0'
as that forces an explicit test for a zero value transferred, independent of compiler and CPU type. One step toward real portable code.
*3 - Including x86, which does not set flags according to moves.
s1
is assigned tos
twice: in the variable initialization, and in the for-loop initialization.