1

[NOTE: this is related to Specifying size of enum type in C and What is the size of an enum in C?, but in those questions, the issue was how to minimize the size of the enclosing struct. Here, we want to specify the size of the individual members of the struct, but still get the documentation advantage of typedef'd enums]

I'm implementing the client side of a serial communication protocol. I'd like to use C structs to capture the format of a serial packet, and I'd like to use C enums to define the legal values of the various slots in the packet.

I don't see a way to do both, but since I'm using GCC, it may be possible.

As a hypothetical example of the problem, assume the packet I receive looks like this:

typedef struct {
  uint16_t appliance_type;
  uint8_t voltage;
  uint16_t crc;
} __attribute__((__packed__)) appliance_t;

That's pretty clear, and I'm pretty sure I'll get a struct that is five bytes long. But it doesn't capture the fact that appliance_type and voltage can only take on certain values.

I'd prefer something like this...

typedef enum {
  kRefrigerator = 600,
  kToaster = 700,
  kBlender = 800
} appliance_type_t;

typedef enum {
  k120vac = 0,
  k240vac = 1,
  k12vdc = 2
} voltage_t;

typedef struct {
  appliance_type_t appliance_type;
  voltage_t voltage;
  uint16_t crc;
} appliance_t;

...but there's no way that I know of to specify that appliance_type_t is 16 bits and voltage_t is 8 bits.

Is there a way to have my cake and eat it too?

update:

I need to make it clear that I'm NOT expecting the compiler to enforce the enum'd values as setters for the respective fields! Rather, I find that typedef'd enums are a useful construct for maintainers of the code, since it makes explicit what the intended values are.

Note: As I researched this, I noticed that GCC enums accept __attribute__ specifications, but I'm not sure that helps.

Community
  • 1
  • 1
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • 3
    You should get rid of packed structs and serialize/deserialize your packets.... – LPs Mar 27 '17 at 06:49
  • 1
    The enumeration constants have type `int` anyway. You can assign arbitrary integers to an enumeration variable. It doesn't provide the type safety you think it does. – StoryTeller - Unslander Monica Mar 27 '17 at 06:50
  • in C++ it's possible to set the [type of enum members](http://stackoverflow.com/a/12125660/995714) but I don't know if there's anything similar in C or not – phuclv Mar 27 '17 at 06:55
  • @StoryTeller Type safety depends on the interface design. You _can_ use enums to get increased type safety even in C. `void func (appliance_type_t* at);` ... `int x; func(&x); // error`. – Lundin Mar 27 '17 at 07:01
  • 2
    @Lundin - The pedant in me wants to argue that it's the type safety given by pointer types, and not enumeration types with regard to their constants :) – StoryTeller - Unslander Monica Mar 27 '17 at 07:08
  • @Lundin I also agree on the part of Storyteller, there is no restriction what value is stored and afaik there will be no errors (maybe warnings but I wont bet on it), if you just pass a value not in the enum as the pointee. I had the case rescently as my Testprogramm could write arbitrary data to memory in front of a certain array because a dev didn't check for x>0 for his paramter which was an enum. – Kami Kaze Mar 27 '17 at 07:43
  • 2
    @StoryTeller This inspired me to come up with a little trick to solve this, here: http://stackoverflow.com/questions/43043246/how-to-create-type-safe-enums – Lundin Mar 27 '17 at 09:55

3 Answers3

1

One way of doing this would be to use the bit field with the definition of the member in the struct.

So your struct would be -

typedef struct {
    appliance_type_t appliance_type:16;
    voltage_t voltage:8;
    uint16_t crc;
} appliance_t;

But this will leave a padding after the voltage field (depends on the implementation of the compiler though). The packed attribute should help you with that.

Ajay Brahmakshatriya
  • 8,993
  • 3
  • 26
  • 49
  • 3
    This is completely non-standard C. Bit-fields in general is a very bad idea, since they are so poorly specified, with non-existent portability. – Lundin Mar 27 '17 at 06:54
  • However much we hate it, we are surrounded by non standard code. But yes, we shouldn't contribute to it. – Ajay Brahmakshatriya Mar 27 '17 at 06:55
0

I would suggest that you keep the "raw data" separate from the abstraction. For example:

typedef struct {
  uint16_t type;
  uint8_t  voltage;
  uint16_t crc;
} __attribute__((__packed__)) raw_data_t;

typedef struct {
  appliance_type_t type;
  voltage_t voltage;
  uint16_t crc;
} appliance_t;

inline appliance_t raw_data_to_appliance (const raw_data_t* raw)
{
  appliance_t app =
  {
    .type    = (appliance_type_t)raw->type,
    .voltage = (voltage_t)raw->voltage,
    .crc     = raw->crc,
  };
  return app;
}

This shouldn't yield much overhead code. See also How to create type safe enums?

Community
  • 1
  • 1
Lundin
  • 195,001
  • 40
  • 254
  • 396
  • 1
    Step between this and doing proper serialization is not that big: It's not worth using compiler extension over portable serialization functions, in my opinion. – user694733 Mar 27 '17 at 07:04
  • 1
    moreover `packed structs` mean `unaligned memory access`, which should be avoided.... – LPs Mar 27 '17 at 07:30
  • @LPs "packed structs should be avoided". Oh, I agree, but try telling that to my ARM processor with limited RAM and FLASH... :) – fearless_fool Mar 27 '17 at 11:09
  • 1
    @fearless_fool That is not necessarily tradeoff you need to make. If member placement is unfavorable for the for the alignment and structures are used a lot, you may end up using *more* flash memory with packed structs. – user694733 Mar 27 '17 at 11:36
  • Using serialization/de-serialization is generally a good idea though. I used used the struct provided in the question. If it was just a raw `uint8_t []` it would be a different story. (But then you get in "strict aliasing" trouble instead.) – Lundin Mar 27 '17 at 11:37
  • @fearless_fool that sentence was referred to _unaligned memory access_. BTW you should avoid packed structs too, but I agree, sometimes (rarely), in embedded systems, you can ran out of memory quickly. ;) – LPs Mar 27 '17 at 12:07
  • I'll give the check here, but it's really a vote in favor of your answer in http://stackoverflow.com/questions/43043246/how-to-create-type-safe-enums – fearless_fool Mar 27 '17 at 15:13
0

So I understand that when you declare an enum, you have no control over its size and in

typedef struct {
  appliance_type_t appliance_type;
  voltage_t voltage;
  uint16_t crc;
} appliance_t;

you want e.g. voltage_t voltage to be of a specific size, let's say uint8_t but you are not able to specify that.

I don't know if you can provide size constraints on an enum, but there are two mechanisms you can use. In both mechanisms you specify unit8_t voltage as the member of the struct:

typedef struct {
  uint8_t appliance_type;
  uint8_t voltage;
  uint16_t crc;
} appliance_t;

Method 1 uses plain old #defines. You just #define the values and use the symbolic constants in assignments:

#define k120vac 0
appliance.voltage= k120vac;

In method 2 you have the enums, but still declared a member of the size you want. Now you can assign the enum values to the member, e.g.:

appliance.voltage= k120vac;


Note: I ran i simple test (VC2008) and also with voltage_t voltage; declaration, you can do appliance.voltage= kToaster; so not even gives the compiler "enum type safety" (checking only values from the domain are assigned/used), so not even that has been lost with the 2 methods I showed.
Did some more tests:
unsigned char x = k12vdc;    // <== OK
unsigned char y = kToaster;  // <== warning: truncation of int to unsigned char

So it seems the compiler uses the smallest type possible for the values of the enum when assigning.

There seems no size associated with the enum itself, only with its individual values. Redefining k12vdc= 2000 now also gives a warning on the assignment x= k12vdc but not on x=k120vac.

Now, given that the compiler (VC2008) doesn't enforce type safety of enums, and that you can't control the size of a variable or member of an enum type, there seems no use in declaring a variable or member of enum type, except maybe for documentation purposes. A variable/member of enum type will be the size of an int, as the following example shows:

appliance_type_t a;
voltage_t v;
printf("sizeof appliance_type_t= %d\n", sizeof(a));  // prints '4'
printf("sizeof voltage_t= %d\n", sizeof(v));         // prints '4'
Paul Ogilvie
  • 25,048
  • 4
  • 23
  • 41
  • How does this answer the question or give information OP didn't already know? OP asked for exact member sizes *and* type safety. Neither of the gives you the latter. – user694733 Mar 27 '17 at 07:49
  • @user694733, and as my test showed on VCV2008, _neither are possible_. Of an enum, you cannot control the size, and the compiler doesn't guard against wrong assignments. So maybe my answer should have been "No, it is not possible"? Or is it possible on another compiler? – Paul Ogilvie Mar 27 '17 at 08:07
  • @PaulOgilvie: sorry if I misled you -- i've updated the OP to make it clear that I'm NOT looking for "type safety" as you call it. See "update" there... – fearless_fool Mar 27 '17 at 11:07
  • Fearless Fool, I did some more tests. See the outcome in my updated answer: There seems no size associated with the enum itself, only with its individual values. – Paul Ogilvie Mar 27 '17 at 12:12