cgcctype-conversioncpu-architectureunsigned-integer

Negative value forced zero when assigned to uint16_t variable in C


I understand that unsigned and signed integers are just a different representation of the underlying bits according to two's complement. That is, the following is my observation -- b is a non-zero integer, good:

// gcc main.c -o main.out && ./main.out
#include <stdio.h>
#include <stdint.h>
void main() {
  int16_t a = -42;
  uint16_t b = a;
  printf("a = %d\n", a); // a = -42
  printf("b = %d\n", b); // b = 65494
}

Now, I have a colleague who claims to have evidence that in our embedded software project (I could not get the compiler toolchain details so far), b may be limited to b = 0 instead!

Processor seems to be an S32G.

Question: Is there any setting in the GCC compiler toolchain, or is there a CPU architecture, that makes unsigned to signed value assignment a limiting operation?

I have tried to compile with -ftrapv to force prevent the underflow, but that produces the same result as above...

UPDATE: I have found the error to be elsewhere: an intermediate assignment had cast float to uint16_t, which appears to be a wholly different problem with possibility of UB. The question is answered. I was more like:

// gcc main.c -o main.out && ./main.out
#include <stdio.h>
#include <stdint.h>
void main() {
  float a = -42;
  uint16_t b = a;
  printf("a = %.1f\n", a); // a = -42.0
  printf("b = %d\n", b); // b = ???
}

Solution

  • ISO C requires this program to print 65494 ; conversion from an integer type to an unsigned integer type is required to modulo-reduce into the value-range of the destination type. (In this case add 2¹⁶ to bring it into the 0..65535 range). Conversion from an FP type doesn't have this guarantee; out-of-range is UB for FP→integer.

    (Except if int is only 16-bit, then the default argument promotions for variadic functions won't promote uint16_t to int, so the arg type will be unsigned and won't match the %d conversion, in which case the behaviour is undefined. Of course in practice it would just print -42 on a 2's complement system. Use %u with (unsigned int)b or use PRIu16 in the format string to make it well-defined everywhere. Thanks to Eric for pointing this out.)


    In practice on 2's-complement systems, this just means truncating the bit-pattern if necessary, or sign-extending or zero-extending a narrow source.

    So no, unless GCC has a nasty and easy-to-detect bug that would have affected a lot of code that casts negative numbers to unsigned and back, e.g. for bithacks.

    Clamping negative numbers to zero would require an extra instruction on all ISAs, since int16 to uint16 requires zero. (The GNU dialect of C guaranteed 2's complement signed integers long before C23. GCC doesn't support any non-2's-complement targets.)

    I have tried to compile with -ftrapv to force prevent the underflow

    There is no signed-integer overflow here. And BTW, even conversion to a signed integer type isn't UB as long as the source is also integer (not FP for example), although the result for out-of-range is implementation-defined in older C versions like C17.

    C++23 changed to require 2's-complement representation, and simplified the rule so even conversion to signed integer modulo-reduces by a power of 2. C23 also requires / guarantees 2's complement, but I haven't checked the wording on conversion to signed integer. Hopefully it's the same; C and C++ try to stay in sync with each other when it makes sense.


    Re: your edit mentioning float:

    (Godbolt) GCC for ARM uses vcvt.u32.f32 as the first step in converting float to uint16_t. (I used volatile so the conversion would happen at run-time, not compile-time constant propagation.)

            vcvt.u32.f32    s16, s16
            vmov    r3, s16 @ int
            uxth    r1, r3
    

    This instruction truncates the float value (rounding toward zero) and gives the nearest representable integer value to that. This means it saturates to the value-range of uint32_t. So negative numbers become zero. (TODO: find an ARM manual that says that. ARM's online HTML references that come up in google searches aren't very detailed, not mentioning any of that.)