强制在结构中枚举字段的大小
强制在结构中枚举字段的大小
我正在实现一个串行通信协议的客户端。我希望使用C结构体来捕获串行数据包的格式,并且我希望使用C枚举来定义数据包中各个字段的合法值。
我没有找到同时满足这两个条件的方法,但由于我使用的是GCC编译器,可能是有可能的。
以一个假设性的问题为例,假设我接收到的数据包如下所示:
typedef struct { uint16_t appliance_type; uint8_t voltage; uint16_t crc; } __attribute__((__packed__)) appliance_t;
这很清晰,我相信我会得到一个长度为五个字节的结构体。但它并没有捕捉到`appliance_type`和`voltage`只能取特定值的事实。
我更倾向于这样的实现:
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;
但据我所知,没有办法指定`appliance_type_t`为16位,`voltage_t`为8位。
是否有办法两全其美呢?
更新:
我需要明确一点,我并不期望编译器将枚举值作为相应字段的设置器进行强制执行!相反,我发现typedef的枚举是代码维护者的有用构造,因为它明确了预期的值是什么。
注意:在我研究过程中,我注意到GCC的枚举接受`__attribute__`规范,但我不确定这是否有帮助。
在上述内容中,提到了在声明枚举时无法控制其大小的问题。在结构体中,希望某个字段的大小为特定的大小,比如uint8_t,但是无法指定。文章中提到了两种解决方法。
第一种方法是使用#define定义常量,并在赋值时使用符号常量:
#define k120vac 0 appliance.voltage= k120vac;
第二种方法是使用枚举类型,并将其声明为所需的大小:
typedef struct { uint8_t appliance_type; uint8_t voltage; uint16_t crc; } appliance_t;
然后可以将枚举值赋值给结构体的成员变量:
appliance.voltage= k120vac;
作者进行了一些测试,发现在赋值时,编译器会使用可能的最小类型来表示枚举值。但是无法控制枚举本身的大小,只能控制枚举值的大小。因此,声明一个枚举类型的变量或成员变量似乎没有用处,除非用于文档目的。枚举类型的变量或成员变量的大小将是int的大小。
无法通过声明枚举类型的变量或成员变量来控制其大小,并且编译器也不会检查错误的赋值。因此,对于这个问题,似乎没有解决方法。
问题的出现原因是想要在一个结构体中强制设置一个枚举字段的大小。解决方法是使用位域和结构体成员定义结合。例如,可以通过使用位域定义结构体的成员来实现。但是这样做可能会导致在电压字段后面留下填充位(具体取决于编译器的实现)。为了解决这个问题,可以使用packed属性。
typedef struct { appliance_type_t appliance_type:16; voltage_t voltage:8; uint16_t crc; } appliance_t;
但是,这种方法是非标准的C语言写法。位域在C语言中是一个非常糟糕的想法,因为它们的规范性很差,并且不具备可移植性。
无论我们多么讨厌非标准的代码,我们都无法避免被它包围。但是,我们不应该为其贡献力量。
枚举(enum)类型的字段在结构体(structure)中被定义时,可能会出现强制设定字段大小的问题。为了解决这个问题,可以将"原始数据(raw data)"与"抽象化(abstraction)"分开。具体的做法是,在定义结构体时,分别使用typedef定义原始数据类型和抽象化类型,然后编写一个将原始数据转换为抽象化类型的函数。此外,还可以使用编译器扩展来确保枚举类型的安全性。
以下是具体的代码示例:
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; }
这种方法不会增加太多额外的代码量。关于枚举类型的安全性,可以参考"如何创建类型安全的枚举?"这个问题。
从这种方法到实现正确的序列化(serialization)并不难:在我看来,使用可移植的序列化函数要比使用编译器扩展更值得。
此外,需要注意的是,使用"packed structs"(紧凑结构体)意味着"unaligned memory access"(非对齐内存访问),应该避免这种情况。
当然,我同意"应该避免使用packed structs",但是试试看告诉这个只拥有有限RAM和FLASH的ARM处理器吧... 🙂
_fool:这不一定是你需要做的权衡。如果成员的位置对于对齐不利,并且结构体被频繁使用,那么使用紧凑结构体可能会导致更多的FLASH内存使用。
使用序列化/反序列化通常是一个不错的选择。我在这里使用了问题中提供的结构体。如果只是一个原始的uint8_t[]数组,情况可能会不同。(但这样会引发"strict aliasing"问题。)
_fool:这句话是指"非对齐内存访问"。顺便说一下,你也应该避免使用紧凑结构体,但是我同意,有时候(很少),在嵌入式系统中,内存可能很快用完。;)
我会在这里给出答案,但实际上是对你在stackoverflow.com上的回答的支持投票。
文章整理完毕。