Hello and thanks for reading,
I just came upon a bug in my C program and while I was able to find out the cause of the bug I am having trouble rationalizing the behaviour of the compiler and I could use some help here. I was working on shifting a range of values into negative values, but was faced with a sign conversion causing it to result in an integer overflow. All in all not too exciting, but this happened because the compiler decided that an enum variable should be unsigned, which was used in a calculation, and then caused the output of that result to be coerced to an unsigned int as well.
Minimum reproducible code:
#include <stdio.h>
enum UnexpectedlyUnsignedEnum {
VALUE1 = 5
};
int main(int argc, char *argv[]) {
enum UnexpectedlyUnsignedEnum enum_value = VALUE1;
int value1 = (1 - enum_value) / 2;
int value2 = (1 - VALUE1) / 2;
printf("%i\n", value1);
printf("%i\n", value2);
}
Which results in the following output:
❯ ./example
2147483646
-2
❯
And I can somewhat understand the compiler trying to "optimize" the type to unsigned int as its values are all positive (I am not sure I agree that it is a good choice to make here, as it seems likely to introduce unnecessary bugs like this without a benefit I can see when the additional range of positive integers is unneeded), but I would still expect value1 and value2 to be equal to one another. That they are not implies to me that it is the storing of the value that converts the variable enum_value to unsigned, whereas the VALUE1 is still signed.
In addition, when compiling with GCC I get the sign conversion warning when enabled, but clang does not provide the warning despite having the flag and producing the same output as above:
GCC
❯ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
...
❯ gcc -Wconversion -o example example.c
example.c: In function ‘main’:
example.c:9:18: warning: conversion to ‘int’ from ‘unsigned int’ may change the sign of the result [-Wsign-conversion]
9 | int value1 = (1 - enum_value) / 2;
|
❯
Clang
❯ clang --version
Ubuntu clang version 18.1.8 (++20240731024944+3b5b5c1ec4a3-1~exp1~20240731145000.144)
...
❯ clang -Wsign-conversion -o example example.c
❯
In short, can someone help me understand whether something is broken here, or what I am failing to comprehend about these seeming discrepancies?
EDIT: Fixing the problem is not my issue, it has already been resolved. I wish to understand why the compiler does it this way as it seems counter-intuitive to me.
This is a known language flaw with enums since forever. The problem is that you are expecting a certain behavior when the language doesn't guarantee it.
Traditionally in C, enumeration constants like VALUE1
in your example, are always of type int
. The enumerated type, enum_value
in your example, has a compiler-specific integer type. The requirement is only this (C17 6.7.2.2):
Each enumerated type shall be compatible with
char
, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined, but shall be capable of representing the values of all the members of the enumeration.
This makes enums dangerous and non-portable if used as part of any form of arithmetic or bitwise operation.
Apparently both gcc and clang for x86_64 decide to use an unsigned type as default. Probably because 32 bit types are more convenient to work with than smaller types, and it can represent the value 5
just fine. Plus there's no need to make the type negative if there are no negative constants in the list. Embedded compilers for low-end targets often use unsigned char
instead, since it is faster and saves memory on 8/16 bit targets.
The new C23 standard finally fixed this mess. Now we can specify a type for the enum explicitly and then both the enumeration variable and the constants turn into that type:
#include <stdio.h>
#include <stdint.h>
enum ExpectedlySignedEnum : int32_t {
VALUE1 = 5
};
int main(int argc, char *argv[]) {
enum ExpectedlySignedEnum enum_value = VALUE1;
int value1 = (1 - enum_value) / 2;
int value2 = (1 - VALUE1) / 2;
printf("%i\n", value1);
printf("%i\n", value2);
}