Given a situation in which USHRT_MAX may equal INT_MAX:
Now suppose "us" is initially set to USHRT_MAX, and USHRT_MAX
is indeed equal to INT_MAX.
And thus the signed int, whose value is INT_MAX, has 1 added
to it. The effect is undefined, and possibly a runtime trap,
which is quite undesirable.
Oh, I see. Thank you for the explanation.
As a side note, to me the whole thing feels like a defect: the committee
took a "shortcut" by defining the increment and compound assignment
operators through their "regular" binary counterparts (instead of
providing independent definitions for them) and ended up with this as an
unintentional side-effect. On the other hand, I could be wrong, since
this specification is consistent with C89/90.
The real flaw, in my opinion, is that the original ANSI C (C89)
committee decided on this bizarre, inconsistent widening treatment of
types in the first place: when an unsigned type is widened, if the
wider signed type can represent all values of the narrower unsigned
type, the resulting type is signed, otherwise the resulting type
is unsigned.
In other words, on a 16-bit implementation, "unsigned short" widens
to "unsigned int", because USHRT_MAX (65535) exceeds INT_MAX (32767)
but not UINT_MAX (65535). On a 32-bit implementation on otherwise
similar (or even identical) hardware, "unsigned short" widens to
*signed* int, because USHRT_MAX (65535) is much less than INT_MAX
(2147483647). Sometimes this makes the code behave differently
on the two compilers, even if they use the same hardware:
unsigned short us = 0;
...
if ((us - 1) > 1)
...
Here, if unsigned short is 16 bits and plain int is also 16 bits
(Compiler A), "us - 1" is (unsigned int)65535, which is greater
than 1. But if unsigned short is 16 bits and plain int is 32 bits
(Compiler B, on the same hardware), "us - 1" is (signed int)-1,
which is less than 1.
The C89 Rationale called such situations "questionably signed";
the theory is that it is hard to tell what the programmer intended
in the first place. So they came up with these so-called "value
preserving" rules. The problem is that, in the presence of any
kind of arithmetic, the value they preserve depends on the relative
values of USHRT_MAX vs INT_MAX (or UINT_MAX vs LONG_MAX, and so
on).
The alternative to "value-preserving" is the so-called "sign
preserving" or "unsigned preserving" rule. This is what Unix-based
systems actually did, and it is CLEARLY (note opinion
) the
better method BY FAR, because it does not require comparing USHRT_MAX
and INT_MAX at all. Instead, a narrow unsigned type *always* widens
to the wider unsigned type.
Note that this completely solves the issue at hand, because then
the fact that "++us" accomplishes the same thing as "us = us + 1"
is not a problem: "us" expands to unsigned int, which has the usual
clock-arithmetic semantics and either goes from 65535 to 65536 or
goes from 65535 to 0 (as appropriate), and then that value is put
back into "us", which always produces 0.
(As far as I can tell, there is only one drawback to "unsigned
preserving" behavior, and that is what happens if plain char is
unsigned. This problem can be solved by fiat: we already know that
the I/O library is problematic if UCHAR_MAX > INT_MAX, so we can
simply rule that UCHAR_MAX < INT_MAX and, if necessary [and I am
not sure whether it is], that plain char violates the "unsigned
preserving" behavior and widens to signed int.)