1

Having this code:

#include <stdio.h>
#include <stdlib.h>

int main (void) {

   typedef enum {bar,baz,last} en;
   en foo = baz;
   for(int i =bar; i<=last ; i++)
      if(i==foo){
         printf("%i\n",i);
      }

   return 0;
}

Why does compiler cares about signedness of enum members? They should be represented from 0 up (e.g bar=0,baz=1,...) and because they are starting from 0 up, they could never be negative, and therefor no reason to case about signedness (unless I assign bar=-1, but do not know if that is possible). So why the error?

Herdsman
  • 799
  • 1
  • 7
  • 24

4 Answers4

1

The compiler cares about signedness because all integer types, including enum types, are either signed or unsigned. For an enum, the exact underlying integer type is definfed by the specific implementation. Section 6.7.2.2p4 of the C standard states:

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.
The enumerated type is incomplete until immediately after the } that terminates the list of enumerator declarations, and complete thereafter.

For gcc in particular, it is defined as follows:

  • The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).

Normally, the type is unsigned int if there are no negative values in the enumeration, otherwise int. If -fshort-enums is specified, then if there are negative values it is the first of signed char, short and int that can represent all the values, otherwise it is the first of unsigned char, unsigned short and unsigned int that can represent all the values.

On some targets, -fshort-enums is the default; this is determined by the ABI.

Because you didn't explicitly set values for your enum, the values start at 0 and go up from there. So assuming you are using gcc, it chooses unsigned int as the underlying type.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • 1
    I'm sure this rationale is correct, but gcc is not being helpful: the whole point of the signedness warnings is to avoid surprises such assigning a too-large unsigned value to a signed int, turning it surprisingly negative (which is a legitimate thing to be concerned about), but in the case of the enums, the compiler *knows* that it can never exceed the range of the target, so it's warning about something that cannot ever be a problem. clang 5.0.1 doesn't warn about this. – Steve Friedl May 05 '20 at 15:07
  • @SteveFriedl If you tried to do something like `if (foo > -1)` you would get a false value because of the conversion of -1 to a large unsigned value. – dbush May 05 '20 at 15:11
  • @SteveFriedl Comparing signed and unsigned types in a sloppy mix is code smell and the compiler is doing a good job if it is producing a diagnostic. gcc and clang aren't really reliable for spotting that code smell though, nor is `-Wconversion`. – Lundin May 05 '20 at 15:14
  • 1
    @dbush Of course you're right about that example - I wasn't saying the warning is wrong *in general*, only that the specific case in the OP's example, none of those cases has any actual signedness concerns. Gcc is correct, just not helpful. When I try the `foo > -1` example, both gcc and clang object, which *is* helpful. – Steve Friedl May 05 '20 at 15:15
  • @Lundin I argue that the compiler is only being helpful if your goal is to satisfy a language lawyer, and the code presented has **no** signedness concerns whatsoever. I agree that many people play fast and loose with signedness, it's definitely sloppy and often buggy/insecure, but this is not one of those cases. Spurious warnings undermine confidence in the compiler, and throwing casts around just to shut the compiler up doesn't actually make the code safer, it just shuts up the compiler. – Steve Friedl May 05 '20 at 15:58
  • @SteveFriedl The compiler would have to do some static analysis in order to determine whether the warning is "useful" or not. Of course you could make the argument that it could do that ... in which case I'd invite you to submit a gcc patch :) The status quo is that the warning appears whenever a sign mismatch occurs in comparison except for a handful of ad hoc cases where the static analysis was easy. – M.M May 06 '20 at 01:20
  • Imagine if the code were `for(int i = -1; i<=foo ; i++)` . Then the loop would never enter, since `-1 <= (some unsigned int value)` is false . So the analysis of `i<=foo` would have to go back and prove that `i` never held a negative value at that point. – M.M May 06 '20 at 01:21
1

Why does compiler cares about signedness of enum members?

It is not the signedness of an enum per se that the compiler cares about here but the signedness of the things you are comparing.

When you have x == y, and x is a signed integer and y is an unsigned integer, they are not compared directly. The rules of C say that one must be converted to the type of the other. If the signed integer type is narrower (technically, has lesser conversion rank) than the unsigned type, it is converted to the unsigned type.

This conversion can change the value of the number, and that can produce a result different from what you desired. For example, -3 < 4u will produce false (0), because converting −3 to unsigned produces a large value (4,294,967,293 in common C implementations). Or -3 == 4294967293u would produce true (1).

So the compiler is warning you that, because of the difference in signedness, the comparison might not behave as you desire.

In this case, the compiler has based its warnings solely on the types of the operands. Looking at the code, we can see that neither of the operands will have a negative value, so the specific values you are comparing will not be affected by this problem. But making that distinction is apparently beyond the capabilities of the compiler you are using.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • "based its warnings solely on the types of the operands" --> I do not think it is _only_ type concern. For me, `i<=last` did not warn `int` compare `en`, even if `i` was changed to `unsigned`. `i==foo` warns as `int` compare `en`. As one compare is a `i` compare `en` constant and the other a `i` compare `en` variable seems to come into play too as suggested by [@lundin](https://stackoverflow.com/a/61616335/2410359). – chux - Reinstate Monica May 05 '20 at 15:32
  • @chux: `i` and `last` have the same type. `i` and `en` do not. – Eric Postpischil May 05 '20 at 15:47
  • True in OP's case, yet as commented, "even if `i` was changed to `unsigned`", `i` and `last` have different types and still no warning. It is more than a type issue. – chux - Reinstate Monica May 05 '20 at 15:49
  • @chux: It may be the context of the `for` loop. Or that, for, when comparing either a signed or unsigned `i` to the constant `last`, the compiler is able to use information about bounds, but it cannot when comparing `i` to the object `en`. – Eric Postpischil May 05 '20 at 17:46
  • In experimenting with variations on OP's code, it appears the fact that `i` (as `int` or `unsigned`) when comparing equality to a positive constant (`int`, `unsigned`, or `en`) does not warn - hence the warning is not solely on operand types. That is one part. Analyzability is another. Example `if ((i&1) == foo) {` does not warn - and that is not in a `for`. I like the rest of the answer. – chux - Reinstate Monica May 05 '20 at 18:24
  • @chux-ReinstateMonica, I dont get, why is a problem, when compilator does not warn. I would assume, the when no warn, then no problem in compilation. So based on what is this statement: `when comparing equality ...does not warn - hence the warning is not solely on operand types` ? – Herdsman May 06 '20 at 00:16
  • @Herdsman This answer's premise is "compiler is warning you that, because of the _difference in signedness_". Yet, for example, `(i&1) == foo`, also a difference in signed-ness does not warn even with an `int` of the left and a `en` on the right. Thus "based its warnings solely on the types of the operands." seems to need something more. – chux - Reinstate Monica May 06 '20 at 00:25
  • It should remain the same. I think when you `and` `i` with `1`, the var `i` will become unsigned (the signed bit is zeroed) and `foo`, which contains unsigned members of type `int` could compare the var `i`, because now are both unsigned and thus no conversion. But I do not know, whether by `i&1` you really "destroy" the sign bit, its just my assumption. – Herdsman May 06 '20 at 00:39
  • @Herdsman: Performing a logical AND of `i` with 1 does not remove the sign bit; it sets the sign bit to zero. When we say something is unsigned here, we are referring to the type. The `int` type is signed: It has a sign bit, which may be 0 or 1. The `unsigned` type is unsigned: It does not have a sign bit. – Eric Postpischil May 06 '20 at 00:48
0

You could as well have done typedef enum {bar=-1,baz,last} en; and potentially gotten a different enumerated type.

It's important to know that C is dysfunctional and inconsistent when it comes to the type of enums. The enumeration constants (bar etc) are guaranteed to always be type int, but the enumerated type (en) may have any implementation-defined integer type that fits all the constants in the list. It may be signed or unsigned. From the C standard 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.

You can have different types for different enums in the same program. The rationale was perhaps that they wanted to save space. But this comes at the cost of portability and non-deterministic typing.

The compiler probably gives a general warning here without checking what internal type en actually equals. You can check yourself with a C11 macro:

#define type_print(x) _Generic((x),                                  \
                               int:          puts("int"),            \
                               unsigned int: puts("unsigned int"),   \
                               default:      puts("something else"))

type_print(foo);

This gives me unsigned int on gcc x86, and so i==foo triggers a signed vs unsigned conversion warning. Best way to fix the problem is to do for(en i=bar; ... instead.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • Although this answer comes closest to a full explanation, it is interesting that `i<=last` does not warn, even if `i` was `unsigned` or `int`. It is more than only a type difference - it appears to be the ability for the compiler to analyse the possibilities. – chux - Reinstate Monica May 05 '20 at 15:42
0

Although the problem is small, is has a lot going on

typedef enum {bar,baz,last} en;
en foo = baz;
for(int i =bar; i<=last ; i++)
  if(i==foo){

As well answered by @lundin:
* The en constants are int
* Objects of type en are char, a signed integer type, or an unsigned integer type.

Code has 2 compare function with int and en.

i<=last
i==foo

Yet As I understand, only the 2nd one warns about "comparison of integer expressions of different signedness: ‘int’ and ‘en’".

In the first case, i<=last: last is a en constant, so that is simple int <= int --> no warning.

But surprisingly had code been unsigned i, i<=last does not warn even it is now a "integer expressions of different signedness".

Changing the last is still a "integer expressions of different signedness" yet no warning.

// i==foo
(i&1)==foo

Conclusion "integer expressions of different signedness" warning happens when 2 conditions are met:

  • Types are of different sign (no rocket science on this one).
  • The compiler concludes the range of possible values of the signed side and the range of the values of the unsigned are not both in the positive signed range - or something like that.

Although our human analysis of for(int i =bar; i<=last ; i++) if(i==foo){ printf("%i\n",i); } sees no compare problem here, the compiler missed it.

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • why is `unsigned i` and `last` different `signedness`? both are `unsigned` (thanks to @Ludin, that proved by `_Generic` that either constans, or object `en` are unsigned, and so is `i` (if declared as such)) or am I missing something? And 2) How does `and`ing an `int` change its `signedness` (As asked before in comment, whether `and`ing zeroes the sign(most significant) bit). – Herdsman May 06 '20 at 01:28
  • @Herdsman 1) For me `type_print(last);` printed "int" as it should per the C spec. "An identifier declared as an enumeration constant has type `int`." Are you certain `type_print(last);`, not `type_print(foo);` printed "unsigned int"? 2) `&1` did not change signed-ness. It does reduce the range of possible results to 0,1 rather than the entire `int` range. – chux - Reinstate Monica May 06 '20 at 01:37
  • if `i&1` does not change signedness, then why is it even needed? the result won't change by narrowing the range, so what is the purpose of it in this case? – Herdsman May 06 '20 at 01:53
  • @Herdsman `i==foo` generates a warning. `i&1==foo` does not generate a warning. In both cases there is an `int` on the left and `en` on the right. The reason for `i&1==foo` is to demonstrate that signed-ness differences _alone_ is not enough to explain why the warning occurs. It is due to signed-ness differences and the compile's abilities to discern the range of each side. – chux - Reinstate Monica May 06 '20 at 02:04