Carlos Fernandez
Carlos Fernandez

Reputation: 57

gcc enum wrong value

I have a enum typedef and when I assign a wrong value (not in the enum) and print this, it shows me an enum value, not the bad value. Why?

This is the example:

#define attribute_packed_type(x ) __attribute__( ( packed, aligned( sizeof( x ) ) ) )

typedef enum attribute_packed_type( uint16_t ) UpdateType_t
{
    UPDATE_A = 4,
    UPDATE_B = 5,
    UPDATE_C = 37,
    UPDATE_D = 43,

     //   UPDATE_TYPE_FORCE_UINT16 = 0xFFFF,

} UpdateType_t;


UpdateType_t myValue;
uint16_t     bad = 1234;

myValue = bad;

printf( "myValue=%d\n", myValue );

return 1;

and the output of this example is:

myValue=210

If I enable the "UPDATE_TYPE_FORCE_UINT16" into the enum the output is:

myValue=1234

I not understand why the gcc make this. Is this a problem, a bug, or is it normal? If this normal, why?

Upvotes: 1

Views: 1390

Answers (1)

Keith Thompson
Keith Thompson

Reputation: 263237

You've run into a case where gcc behaves oddly when you specify both packed and aligned attributes for an enumerated type. It's probably a bug. It's at least an undocumented feature.

A simplified version of what you have is:

typedef enum __attribute__ (packed, aligned(2)) UpdateType_t {
    foo, bar
} UpdateType_t;

The values of the enumerated constants are all small enough to fit in a single byte, either signed or unsigned.

The behavior of the packed and aligned attributes on enum types is a bit confusing. The behavior of packed in particular is, as far as I can tell, not entirely documented.

My experiments with gcc 5.2.0 indicate that:

  • __attribute__(packed) applied to an enumerated type causes it to be given the smallest size that can fit the values of all the constants. In this case, the size is 1 byte, so the range is either -128..+127 or 0..255. (This is not documented.)

  • __attribute__(aligned(N)) affects the size of the type. In particular, aligned(2) gives the enumerated type a size and alignment of 2 bytes.

The tricky part is this: if you specify both packed and aligned(2), then the aligned specification affects the size of the enumerated type, but not its range. Which means that even though an enum e is big enough to hold any value from 0 to 65535, any value exceeding 255 is truncated, leaving only the low-order 8 bits of the value.

Regardless of the aligned specification, the fact that you've used the packed attribute means that gcc will restrict the range of your enumerated type to the smallest range that can fit the values of all the constants. The aligned attribute can change the size, but it doesn't change the range.

In my opinion, this is a bug in gcc. (And clang, which is largely gcc-compatible, behaves differently.)

The bottom line is that by packing the enumeration type, you've told the compiler to narrow its range. One way to avoid that is to define an additional constant with a value of 0xFFFF, which you show in a comment.

In general, a C enum type is compatible with some integer type. The choice of which integer type to use is implementation-defined, as long as the chosen type can represent all the specified values.

According to the latest gcc manual:

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.

Also quoting the gcc manual:

The packed attribute specifies that a variable or structure field should have the smallest possible alignment -- one byte for a variable, and one bit for a field, unless you specify a larger value with the aligned attribute.

Here's a test program, based on yours but showing some extra information:

#include <stdio.h>
int main(void) {
    enum __attribute((packed, aligned(2))) e { foo, bar };
    enum e obj = 0x1234;
    printf("enum e is %s, size %zu, alignment %zu\n",
           (enum e)-1 < (enum e)0 ? "signed" : "unsigned",
           sizeof (enum e),
           _Alignof (enum e));
    printf("obj = 0x%x\n", (unsigned)obj);
    return 0;
}

This produces a compile-time warning:

c.c: In function 'main':
c.c:4:18: warning: large integer implicitly truncated to unsigned type [-Woverflow]
     enum e obj = 0x1234;
                  ^

and this output:

enum e is unsigned, size 2, alignment 2
obj = 0x34

The simplest change to your program would be to add the

UPDATE_TYPE_FORCE_UINT16 = 0xFFFF

that you've commented out, forcing the type to have a range of at least 0 to 65535. But there's a more portable alternative.

Standard C doesn't provide a way to specify the representation of an enum type. gcc does, but as we've seen it's not well defined, and can yield surprising results. But there is an alternative that doesn't require any non-portable code or assumptions beyond the existence of uint16_t:

enum {
    UPDATE_A = 4,
    UPDATE_B = 5,
    UPDATE_C = 37,
    UPDATE_D = 43,
};
typedef uint16_t UpdateType_t;

The anonymous enum type serves only to define the constant values (which are of type int, not of the enumeration type). You can declare objects of type UpdateType_T and they'll have the same representation as uint16_t, which (I think) is what you really want.

Since C enumeration constants aren't closely tied to their type anyway (for example UPDATE_A is of type int, not of the enumerated type), you might as well use the num declaration just to define the values of the constants, and use whatever integer type you like to declare variables.

Upvotes: 3

Related Questions