ctype-conversionportabilitysignedtype-narrowing

What is the safest and most well-defined way to perform narrowing type conversions with signed integers in C?


I have read: What happens when I assign long int to int in C?

int64_t x;
int32_t y;

y = (int32_t)x;

According to the answer and to the C standard if I try to assign, for example, a signed long to a signed int and the value does not fit, the result is implementation-defined or an implementation-defined signal is raised.

To solve this poorly defined behavior I thought about using unsigned types first:

y = (int32_t)(uint32_t)(uint64_t)x;

But this is no good either because like with converting larger integer types to smaller ones, converting an unsigned integer to a signed one will result in an implementation-defined value or signal if the value does not fit.

I would like to perform narrowing conversions such as unsigned to signed or a larger signed integer to a smaller one, in a way that does not invoke implementation defined behavior, and will simply preserve the bit representations (this should give very predictable and well defined results as long as the sign representation is as expected). Is there any way to do this with casts? Will I have to use a union like so?

union Cast {
    int32_t i32;
    int64_t i64;
};
y = ((union Cast){.i64 = x}).i32;

Will that even work (i.e. guarantee that y simply contains the exact bit pattern as the lower 32 bits of x)? As a last resort will memcpy() have to be used and will that work?

Edit: Turns out using the above union will not be portable because whether that gets the upper or lower 32 bits depends on whether the system is big or little endian. However if I use this:

union Cast {
    int32_t i;
    uint32_t u;
};
y = ((union Cast){.u = (uint32_t)x}).i;

Would that be portable?

Note: This question was asked out of curiosity, and the fact that many times, for diverse reasons, I want to safely truncate the bits of a signed integer.


Solution

  • I would like to perform narrowing conversions… in a way that… will simply preserve the bit representations (this should give very predictable and well defined results as long as the sign representation is as expected).

    Well, that is on the right track, but it is missing something: You did not say what sign representation is expected. An expert tip for writing specifications is to actually specify what you want.

    Let’s say you want two’s complement, because it is the most popular representation scheme. Equivalently, given some integer x you want to convert to a signed N-bit integer format, you want a result y congruent to x modulo 2N with −2N−1y < 2N−1.

    The intN_t types are guaranteed to use two’s complement and have no padding bits (C 2018 7.20.1.1 1). So, if you can get the low bits of x into a intN_t, you are done. (I will also note that given a signed integer type and its corresponding unsigned integer type, the standard requires that their corresponding value bits represent the same value, in C 2018 6.2.6.2 2.)

    You cannot rely on a union with a wider type because the C standard does not specify the order of bytes in storage, so you do not know, from the C standard alone, which bytes of the wider type contain its lower bits. We can work around this easily by converting the value to a uintN_t first. The conversion from integers out of range of an unsigned type to the unsigned type is fully defined by the C standard; it wraps modulo 2N, producing a result in [0, 2N).

    Here we perform that conversion and use a union in a compound literal to reinterpret its result with two’s complement:

    (union { uintN_t u; intN_t i; }) {x} .i

    Although C++ is not mentioned in the question, I will note that reinterpreting via a union is not defined by the C++ standard. An alternative is to copy bytes:

    uintN_t u = x;
    intN_t i;
    memcpy(&i, &u, sizeof i);
    

    Both of these are portable to any implementations that support the desired fixed-width types.

    This can also be done using arithmetic rather than reinterpreting bytes. Say we are converting from some wider type with M bits. First we can cast to uintN_t to get the low bits. Then we can adjust the value represented by bit N−1: ((uintN_t) x ^ ((intM_t) 1 << N-1)) - ((intM_t) 1 << N). That arithmetic is explained in this answer.

    We can also use:

    uintN_t u = x;
    intN_t i = u - (intM_t) u >> N-1 << N;
    

    That arithmetic is explained in this answer.