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 in r
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 an int
:
std::size_t
is the unsigned integer type of the result of the sizeof operator. [..] std::size_t
can store the maximum size of a theoretically possible object of any type (including array).
And, just to get it out of the way, the expression -1
is unary -
applied to the literal 1
, and has type int
on any system:
[C++11: 2.14.2/2]:
The type of an integer literal is the first of the corresponding list in Table 6 in which its value can be represented. [..]
(I won't cite all the text that describes how applying unary -
to an int
results in an int
, 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 hold std::numeric_limits<std::size_t>::max()
.
Now, what happens to those operands?
[C++11: 5.10/1]:
The ==
(equal to) and the !=
(not equal to) operators have the same semantic restrictions, conversions, and result type as the relational operators except for their lower precedence and truth-value result. [..]
[C++11: 5.9/2]:
The usual arithmetic conversions are performed on operands of arithmetic or enumeration type. [..]
Let's examine these "usual arithmetic conversions":
[C++11: 5/9]:
Many binary operators that expect operands of arithmetic or enumeration type cause conversions and yield result types in a similar way. The purpose is to yield a common type, which is also the type of the result.
This pattern is called the usual arithmetic conversions, which are defined as follows:
- If either operand is of scoped enumeration type (7.2), no conversions are performed; if the other
operand does not have the same type, the expression is ill-formed.
- If either operand is of type
long double
, the other shall be converted to long double`.
- Otherwise, if either operand is
double
, the other shall be converted to double
.
- Otherwise, if either operand is
float
, the other shall be converted to float
.
- Otherwise, the integral promotions (4.5) shall be performed on both operands.59 Then the following rules shall be applied to the promoted operands:
- If both operands have the same type, no further conversion is needed.
- Otherwise, if both operands have signed integer types or both have unsigned integer types, the
operand with the type of lesser integer conversion rank shall be converted to the type of the
operand with greater rank.
- Otherwise, if the operand that has unsigned integer type has rank greater than or equal to the rank of the type of the other operand, the operand with signed integer type shall be converted to
the type of the operand with unsigned integer type.
- Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, the operand with unsigned integer type shall be converted to the type of the operand with signed integer type.
- Otherwise, both operands shall be converted to the unsigned integer type corresponding to the
type of the operand with signed integer type.
I've highlighted the passage that takes effect here and, as for why:
[C++11: 4.13/1]
: Every integer type has an integer conversion rank defined as follows
- [..]
- The rank of
long long int
shall be greater than the rank of long int
, which shall be greater than the rank of int
, which shall be greater than the rank of short int
, which shall be greater than the rank of signed char
.
- The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type.
- [..]
All integral types, even the fixed-width ones, are composed of the standard integral types; therefore, logically, std::size_t
must be unsigned long long
, unsigned long
, or unsigned int
.
If std::size_t
is unsigned long long
, or unsigned long
, then the rank of std::size_t
is greater than the rank of unsigned int
and, therefore, also of int
.
If std::size_t
is unsigned int
, the rank of std::size_t
is equal to the rank of unsigned int
and, therefore, also of int
.
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?
[C++11: 4.7/2]:
If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source integer (modulo 2n where n is the number of bits used to represent the unsigned type). [ Note: In a two’s complement representation, this conversion is conceptual and there is no change in the bit pattern (if there is no truncation). —end note ]
[C++11: 4.7/3]:
If the destination type is signed, the value is unchanged if it can be represented in the destination type (and bit-field width); otherwise, the value is implementation-defined.
This means that std::size_t(-1)
is equivalent to std::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 doing std::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:
std::cout << (std::size_t(-1) == std::numeric_limits<size_t>::max()) << '\n';
// "1"
And, just to illustrate my point from earlier, on my 64-bit system:
std::cout << std::is_same<unsigned long, std::size_t>::value << '\n';
std::cout << std::is_same<unsigned long, unsigned int>::value << '\n';
std::cout << std::hex << std::showbase
<< std::size_t(-1) << ' '
<< std::size_t(static_cast<unsigned int>(-1)) << '\n';
// "1"
// "0"
// "0xffffffffffffffff 0xffffffff"
r--
is not an "underflow", though it's reasonable to refer to it that way informally. – Shinshinaconst bool result = (r == (size_t)-1);
. Just to be sure compiles interprets-1
assize_t
and notr
as some other type variable. But your option might be correct as well, just need take a look at standard. – Swish