Consider the following†:
size_t r = 0;
r--;
const bool result = (r == -1);
Does the comparison whose result initialises result
have well-defined behaviour?
And is its result true
, as I'd expect?
This Q&A was written because I was unsure of two factors in particular.
They may both be identified by use of the term "crucial[ly]" in my answer.
† This example is inspired by an approach for loop conditions when the counter is unsigned:
for (size_t r = m.size() - 1; r != -1; r--)
Yes, and the result is what you would expect.
Let's break it down.
What is the value of
r
at this point? Well, the underflow is well-defined and results inr
taking on its maximum value by the time the comparison is run.std::size_t
has no specific known bounds, but we can make reasonable assumptions about its range when compared to that of anint
:And, just to get it out of the way, the expression
-1
is unary-
applied to the literal1
, and has typeint
on any system:(I won't cite all the text that describes how applying unary
-
to anint
results in anint
, but it does.)It's more than reasonable to suggest that, on the majority of systems, an
int
is not going to be able to holdstd::numeric_limits<std::size_t>::max()
.Now, what happens to those operands?
Let's examine these "usual arithmetic conversions":
I've highlighted the passage that takes effect here and, as for why:
All integral types, even the fixed-width ones, are composed of the standard integral types; therefore, logically,
std::size_t
must beunsigned long long
,unsigned long
, orunsigned int
.If
std::size_t
isunsigned long long
, orunsigned long
, then the rank ofstd::size_t
is greater than the rank ofunsigned int
and, therefore, also ofint
.If
std::size_t
isunsigned int
, the rank ofstd::size_t
is equal to the rank ofunsigned int
and, therefore, also ofint
.Either way, per the usual arithmetic conversions, the signed operand is converted to the type of the unsigned operand (and, crucially, not the other way around!). Now, what does this conversion entail?
This means that
std::size_t(-1)
is equivalent tostd::numeric_limits<std::size_t>::max()
; it's crucial that the value n in the above clause relates to the number of bits used to represent the unsigned type, not the source type. Otherwise, we'd be doingstd::size_t((unsigned int)-1)
, which is not the same thing at all — it could be many orders of magnitude smaller than our desired value!Indeed, now that we know the conversions are all well-defined, we can test this value:
And, just to illustrate my point from earlier, on my 64-bit system:
Strictly speaking, the value of
result
is implementation-defined. In practice, it's almost certain to betrue
; I'd be surprised if there were an implementation where it'sfalse
.The value of
r
afterr--
is the value ofSIZE_MAX
, a macro defined in<stddef.h>
/<cstddef>
.For the comparison
r == -1
, the usual arithmetic conversions are performed on both operands. The first step in the usual arithmetic conversions is to apply the integral promotions to both operands.r
is of typesize_t
, an implementation-defined unsigned integer type.-1
is an expression of typeint
.On most systems,
size_t
is at least as wide asint
. On such systems, the integral promotions cause the value ofr
either to be converted tounsigned int
or to keep its existing type (the former can happen ifsize_t
has the same width asint
, but a lower conversion rank). Now the left operand (which is unsigned) has at least the rank of the right operand (which is signed). The right operand is converted to the type of the left operand. This conversion yields the same value asr
, and so the equality comparison yieldstrue
.That's the "normal" case.
Suppose we have an implementation where
size_t
is 16 bits (let's say it's atypedef
forunsigned short
) andint
is 32 bits. SoSIZE_MAX == 65535
andINT_MAX == 2147483647
. Or we could have a 32-bitsize_t
and a 64-bitint
. I doubt that any such implementation exists, but nothing in the standard forbids it (see below).Now the left side of the comparison has type
size_t
and value65535
. Since signedint
can represent all the values of typesize_t
, the integral promotions convert the value to65535
of typeint
. Both side of the==
operator have typeint
, so the usual arithmetic conversions have nothing to do. The expression is equivalent to65535 == -1
, which is clearlyfalse
.As I mentioned, this kind of thing is unlikely to happen with an expression of type
size_t
-- but it can easily happen with narrower unsigned types. For example, ifr
is declared as anunsigned short
, or anunsigned char
, or even a plainchar
on a system where that type is signed, the value ofresult
will probably befalse
. (I say probably becauseshort
or evenunsigned char
can have the same width asint
, in which caseresult
will betrue
.)In practice, you can avoid the potential problem by doing the conversion explicitly rather than relying on the implementation-defined usual arithmetic conversions:
or
C++11 standard references:
size_t
18.2 paragraphs 6-7:
So there's no prohibition on making
size_t
narrower thanint
. I can almost plausibly imagine a system whereint
is 64 bits, but no single object can be bigger than 232-1 bytes sosize_t
is 32 bits.