Is (int)(unsigned)-1 == -1 undefined behavior

I am trying to understand the meaning of the statement:

(int)(unsigned)-1 == -1;

To my current understanding the following things happen:

  1. -1 is a signed int and is casted to unsigned int. The result of this is that due to wrap-around behavior we get the maximum value that can be represented by the unsigned type.

  2. Next, this unsigned type maximum value that we got in step 1, is now casted to signed int. But note that this maximum value is an unsigned type. So this is out of range of the signed type. And since signed integer overflow is undefined behavior, the program will result in undefined behavior.

My questions are:

  1. Is my above explanation correct? If not, then what is actually happening.
  2. Is this undefined behavior as i suspected or implementation defined behavior.

PS: I know that if it is undefined behavior(as opposed to implementation defined) then we cannot rely on the output of the program. So we cannot say whether we will always get true or false.


Solution 1:

Cast to unsigned int wraps around, this part is legal.

Out-of-range cast to int is legal starting from C++20, and was implementation-defined before (but worked correctly in practice anyway). There's no UB here.

The two casts cancel each other out (again, guaranteed in C++20, implementation-defined before, but worked in practice anyway).

Signed overflow is normally UB, yes, but that only applies to overflow caused by a computation. Overflow caused by a conversion is different.

cppreference

If the destination type is signed, the value does not change if the source integer can be represented in the destination type. Otherwise the result is:

(until C++20) implementation-defined

(since C++20) the unique value of the destination type equal to the source value modulo 2n where n is the number of bits used to represent the destination type.

(Note that this is different from signed integer arithmetic overflow, which is undefined).


More specifics on how the conversions work.

Lets's say int and unsigned int occupy N bits.

The values that are representable by both int and unsigned int are unchanged by the conversion. All other values are increased or decreased by 2N to fit into the range.

This conveniently doesn't change the binary representation of the values.

E.g. int -1 corresponds to unsigned int 2N-1 (largest unsigned int value), and both are represented as 11...11 in binary. Similarly, int -2N-1 (smallest int value) corresponds to unsigned int 2N-1 (largest int value + 1).

int:   [-2^(n-1)] ... [-1] [0] [1] ... [2^(n-1)-1]
            |           |   |   |           |
            |           '---|---|-----------|-----------------------.
            |               |   |           |                       |
            '---------------|---|-----------|----------.            |
                            |   |           |          |            |
                            V   V           V          V            V
unsigned int:              [0] [1] ... [2^(n-1)-1] [2^(n-1)] ... [2^n-1]