Search code examples
c++standardsnumericunsigned-integercompile-time-constant

Why is numeric_limits<uint16_t>::max() not equal to -1?


#include <iostream>
#include <cstdint>

using namespace std;

static_assert(-1 == numeric_limits<uint64_t>::max()); // ok
static_assert(-1 == numeric_limits<uint32_t>::max()); // ok
static_assert(-1 == numeric_limits<uint16_t>::max()); // error

int main()
{
    cout << numeric_limits<uint16_t>::max() << endl;
    cout << uint16_t(-1) << endl;
}

output:

65535
65535

Why is numeric_limits<uint16_t>::max() not equal to -1?

Update:

According to cppref:

Similarly USHRT_MAX may not be of an unsigned type: its type may be int.


Solution

  • Integer conversions and promotions

    The uint16_t value goes through integer promotion whereas for (your particular platform; see below) the uint32_t and uint64_t cases the -1 value (not an integer literal, per se, but the unary minus operator applied to the integer literal 1), goes through integer conversion with a resulting value that is equal to the maximum respective value of the uint32_t and uint64_t types, due to the integer congruence between the source and the destination values of this conversion.

    static_assert(-1 == std::numeric_limits<std::uint64_t>::max());
    //            ^^
    //            | Integer conversion:
    //            |   Destination type: uint64_t
    //            |   Resulting value: std::numeric_limits<std::uint64_t>::max()
    
    static_assert(-1 == std::numeric_limits<std::uint32_t>::max());
    //            ^^
    //            | Integer conversion:
    //            |   Destination type: uint32_t
    //            |   Resulting value: std::numeric_limits<std::uint32_t>::max()
    
    static_assert(-1 == std::numeric_limits<std::uint16_t>::max());
    //                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    //                  | Integer promotion:
    //                  |   Destination type: int
    //                  |   Resulting value: std::numeric_limits<std::uint16_t>::max()
    

    From [expr.eq]/1 and [expr.eq]/6 [emphasis mine]:

    [expr.eq]/1

    The == (equal to) and the != (not equal to) operators group left-to-right. The operands shall have arithmetic, enumeration, pointer, or pointer to member type, or type std​::​nullptr_­t. The operators == and != both yield true or false, i.e., a result of type bool. In each case below, the operands shall have the same type after the specified conversions have been applied.

    [expr.eq]/6

    If both operands are of arithmetic** or enumeration type, the usual arithmetic conversions are performed on both operands; each of the operators shall yield true if the specified relationship is true and false if it is false.

    From [conv.integral]/1 and [conv.integral]/2:

    [conv.integral]/1

    A prvalue of an integer type can be converted to a prvalue of another integer type. A prvalue of an unscoped enumeration type can be converted to a prvalue of an integer type.

    [conv.integral]/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 ]

    This alone should yield the same behaviour for all your three examples. However, what differs for the uint16_t case is that [conv.integral]/5 applies:

    [conv.integral]/5

    The conversions allowed as integral promotions are excluded from the set of integral conversions.

    From [conv.rank]/1

    [conv.rank]/1

    Every integer type has an integer conversion rank defined as follows:

    [...]

    (1.3) 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.

    (1.4) The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type.

    the integer conversion rank of uint16_t (same rank or lower than short int) is lower than that of int, which means that [conv.prom]/1 applies for uint16_t [emphasis mine]:

    [conv.prom]/1

    A prvalue of an integer type other than bool, char16_­t, char32_­t, or wchar_­t whose integer conversion rank is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.


    Platform dependent behaviour

    However, whilst we were able to make an argument for uint16_t above due to the lower bound requirement on the maximum value of an unsigned short int—guaranteeing that uint16_t always has a lower integer conversion rank than int—we cannot make the converse argument for the fact that uint32_t will never have lower integer conversion rank than int, as the ISO C++ Standard places no upper bounds on the maximum value requirement on the fundamental integer types.

    From [basic.fundamental]/2 and [basic.fundamental]/3 [extract, emphasis mine]:

    [basic.fundamental]/2

    There are five standard signed integer types : “signed char”, “short int”, “int”, “long int”, and “long long int”. In this list, each type provides at least as much storage as those preceding it in the list. [...] Plain ints have the natural size suggested by the architecture of the execution environment; the other signed integer types are provided to meet special needs.

    [basic.fundamental]/3

    For each of the standard signed integer types, there exists a corresponding (but different) standard unsigned integer type: “unsigned char”, “unsigned short int”, “unsigned int”, “unsigned long int”, and “unsigned long long int”, each of which occupies the same amount of storage and has the same alignment requirements as the corresponding signed integer type; [...]

    The signed and unsigned integer types shall satisfy the constraints given in the C standard, section 5.2.4.2.1.

    And, from the C11 Standard draft [extract, emphasis mine]:

    5.2.4.2.1 Sizes of integer types <limits.h>

    [...] Their implementation-defined values shall be equal or greater in magnitude (absolute value) to those shown, with the same sign.

    [...]

    • maximum value for an object of type short int: SHRT_MAX +32767

    • maximum value for an object of type int: INT_MAX +32767

    [...]

    Note that these maximum values describes the lower bound on the maximum values that the respective fundamental integer type shall be able to store, whereas there is no requirement placed on the upper bound of these maximum values. Moreover, recall from the [basic.fundamental]/2 quote above that each subsequent fundamental (signed) integer type needs only provide at least as much storage as the one proceeding it (in the list).

    This means that, in theory, a platform could implement short int and int as 32 bit wide and 64 bit wide integers, respectively, meaning that, on this platform, uint32_t would have same integer conversion rank as (unsigned) short int, which would mean a lower conversion rank than int, in which case [conv.prom]/1 would apply also for the uint32_t example on this particular platform.