C11 中能否通过 union 的公共结构体成员读取共享字段?
结论
原写法不能保证在所有 C11 实现中都成立。通过 common 读取共享字段是否受到共同初始序列规则保护,取决于当前存储的结构体中 id 的枚举类型是否与 int 兼容。
如果 CurrentId_t 与 int 兼容,CommonData_t 和 CurrentData_t 的共同初始序列可以包含 id 和 value。对于 VoltageId_t,也需要单独判断它是否与 int 兼容。
枚举不一定与 int 兼容
C11 §6.7.2.2 第 4 段规定,每个枚举类型与 char、某个有符号整数类型或某个无符号整数类型兼容,具体由实现选择。因此,不能仅凭枚举值为 0、1 就认定其兼容类型是 int。参见 C11 委员会草案 N1570。
还要区分枚举常量和枚举对象:在 C11 中,CURRENT_ID_1 等枚举常量具有 int 类型,但这不代表 CurrentId_t 对象的类型就是 int。两个独立定义的枚举,也不能因为取值相同就视为同一个枚举类型。上述区别见同一草案的 §6.7.2.2 和 §6.7.2.3。
为什么 value 也受 id 影响?
共同初始序列必须从结构体的第一个成员开始连续匹配。C11 §6.5.2.3 第 6 段要求对应成员具有兼容类型;如果是位域,还要求宽度相同。该保证适用于能够看见 union 完整类型声明的位置。参见 N1570 §6.5.2.3。
如果第一项 id 不兼容,就不能跳过它,仅凭第二项都是 float value 来套用这项规则。即使某个平台上的字段偏移恰好相同,也不能据此证明原代码可移植。
原访问因此缺少共同初始序列规则的保证,但不能进一步笼统地说,C11 中所有读取非最后写入 union 成员的行为都必然是未定义行为。
如何让 value 的读取具有可移植性?
如果主要需求是统一读取 value,可以把它放到三个结构体的首位。保留原有的枚举定义和 Measurement_t,将三个数据结构修改为:
typedef struct {
float value;
int id;
} CommonData_t;
typedef struct {
float value;
CurrentId_t id;
} CurrentData_t;
typedef struct {
float value;
VoltageId_t id;
} VoltageData_t;
这样,第一个成员都是相同的 float 类型,共同初始序列至少包含 value。使用方式如下:
Measurement_t m = {
.kind = CURRENT,
.current = {
.value = 1.25f,
.id = CURRENT_ID_1
}
};
float value = m.common.value;
CurrentId_t id = m.current.id;
这里通过 common.value 读取共享值,通过实际存储的 current.id 读取枚举标识。这个修改不保证 common.id 也能跨成员读取。
如果还希望统一读取 id,可以将三个结构体的 id 都声明为 int,使对应字段类型保持一致;代价是这些字段不再使用各自的枚举类型声明。也可以把共享字段移到外层结构体中,让 union 只保存各分支独有的数据。
使用时的注意事项
kind需要由程序维护。给它赋值不会自动初始化或切换 union 中的数据。- 共同初始序列规则保证的是相应部分的读取,不能据此随意通过
common写入后继续按其他分支解释。 - 如果保留原布局,应依据目标实现对枚举兼容类型的规定判断。一次编译通过、一次运行结果正确,或
sizeof相同,都不足以证明类型兼容。
备注:内容仅供参考。
