C++联合体成员访问和未定义行为

5

我目前正在处理一个项目,其中提供了以下结构。我的工作是C ++,但该项目同时使用C和C ++。相同的结构定义被C和C ++使用。

typedef struct PacketHeader {
    //Byte 0
    uint8_t  bRes                           :4;
    uint8_t  bEmpty                         :1;
    uint8_t  bWait                          :1;
    uint8_t  bErr                           :1;
    uint8_t  bEnable                        :1;
    //Byte 1
    uint8_t  bInst                          :4;
    uint8_t  bCount                         :3;
    uint8_t  bRres                          :1;
    //Bytes 2, 3
    union {
        uint16_t wId;    /* Needed for Endian swapping */
        struct{
            uint16_t wMake                  :4;
            uint16_t wMod                   :12;
        };
    };
} PacketHeader;

根据结构体实例的使用方式,所需的结构体字节序可能是大端或小端。由于结构体的前两个字节是单个字节,当字节序发生改变时,它们不需要更改。 存储为单个 uint16_t 的第二个和第三个字节是我们需要交换以实现所需字节序的唯一字节。为了进行字节序交换,我们一直在执行以下操作:

//Returns a constructed instance of PacketHeader with relevant fields set and the provided counter value
PacketHeader myHeader = mmt::BuildPacketHeader(count);

uint16_t packetIdFlipped;
//Swap positions of byte 2 and 3
packetIdFlipped = myHeader.wId << 8;
packetIdFlipped |= (uint16_t)myHeader.wId >> 8;

myHeader.wId = packetIdFlipped;

函数BuildPacketHeader(uint8_t)明确为成员wMakewMod赋值,并且不会写入成员wId。我的问题是关于在返回的结构实例内读取成员wId的安全性。
诸如Accessing inactive union member and undefined behavior?Purpose of Unions in C and C++我拥有的草案标准的第10.4节中都提到了在C++中访问联合的非活动成员时产生的未定义行为。
链接草案的第10.4节第1段还包含以下说明,尽管我无法理解使用的所有术语:

[注意:为了简化联合的使用,作出了一个特殊的保证:如果标准布局联合包含多个共享公共初始序列(10.3)的标准布局结构体,并且此标准布局联合类型的对象的非静态数据 成员处于活动状态并且是其中一个标准布局结构,则允许检查任何标准布局结构成员的公共初始序列。参见10.3。 - 结束注意]

packetIdFlipped = myHeader.wId << 8行中读取myHeader.wId是否未定义行为?
无名结构体是否是活动成员,因为它是函数调用中最后一个编写的成员?
还是说明意味着访问wId成员是安全的,因为它和结构体共享通用类型?(这是公共初始序列所指的吗?)
提前感谢您。

1
C++中的匿名结构是不完整的。例如:struct{ /* 成员 */ }; - eerorika
@eerorika 谢谢 - 我甚至没有意识到这一点。研究它导致了一个注释_类似于union,一个没有名称的结构体成员被称为匿名结构体。匿名结构体的每个成员都被视为封闭结构体或联合体的成员。如果封闭结构体或联合体也是匿名的,则递归应用此规则_ 在此处。这是否意味着上面的代码实际上_不是_UB,因为内部结构体和联合体的每个成员都被认为是struct PacketHeader的成员? - cprlkleg
它是未定义行为。联合体匿名并不改变任何事情。 - eerorika
好像找不到编辑评论按钮,但仔细阅读后,我认为那个链接无论如何都是指的C语言。再次感谢。 - cprlkleg
3个回答

4
函数 BuildPacketHeader(uint8_t) 显式地为成员 wMake 和 wMod 分配值,并且不写入成员 wId。我的问题是在结构体中返回的实例中读取成员 wId 的安全性。

是的,这是未定义行为(UB)。这并不意味着它不起作用,只是可能不起作用。您可以在 BuildPacketHeader 中使用 memcpy 来避免这种情况(请参见这里这里)。


感谢您的回复和memcpy提示。考虑到内部联合和结构体都是匿名的(正如eerorika所指出的),这仍然是UB吗?以下评论类似于union,一个类型为没有名称的结构体的结构体的未命名成员被称为匿名结构体。匿名结构体的每个成员都被视为封闭结构或联合的成员。如果封闭结构或联合也是匿名的,则递归应用此规则。 - cprlkleg

1

packetIdFlipped = myHeader.wId << 8 这一行中读取 myHeader.wId 是否是未定义的行为?

是的。您给 wMakewMod 赋值,使得匿名结构体成为活动成员,因此 wId 是非活动成员,您不能在没有为其设置值的情况下从中读取。

这是否就是所谓的公共初始序列

当两个标准布局类型以相同顺序共享相同成员时,即为 公共初始序列

struct foo
{
    int a;
    int b;
};

struct bar
{
    int a;
    int b;
    int c;
};

foobar中的ab是相同类型,因此它们是它们的公共初始序列。如果将foobar的对象放在一个联合体中,则在其中一个对象中设置后,从任一对象中读取ab是安全的。

然而,这不适用于您的情况,因为wId不是标准布局类型结构。


wId包装在struct ID{ ... }中是否足以提供标准布局类型?如果是这样,它是否会与以下匿名(在提供的示例中 - 现在已更改,因为eerorika指出它是不合法的)结构共享公共初始序列,因为每个结构都包含一个uint16_t初始成员?或者后面那个结构的成员是位域意味着它们不是常见类型? - cprlkleg
在提出更多问题之前,我应该更仔细地阅读,对此我深表歉意。您发布的链接链接到另一个页面,澄清了“如果它们是位域,则它们具有相同的宽度”。 - cprlkleg
1
@cprlkleg 这仍然是不合法的。两个结构体都必须有一个位域才能合法,正如你发现的那样。 - NathanOliver

1

根据C++标准,给定两个结构体A和B以及下面的union:

union U
{
  A a;
  B b;
};

以下是有效的代码:
U u;

A a;
u.a = a;
a = u.a;

B b;
u.b = b;
b = u.b;

你读取和写入相同类型。这是显然正确的代码。
但是当你有以下代码时,问题就出现了:
A a;
B b;
u.a = a;
b = u.b;

我们对A和B有什么了解?首先,在联合中,它们共享同一内存空间。现在,C++标准已明确将其声明为未定义行为。
但这并不意味着完全没有用处。 C99发挥了作用,因为它是规范基础,并且对于联合体有弱保证。也就是说,如果联合成员具有相同的内存布局,则它们是兼容的,并且每个结构的第一个内存地址相同。因此,如果您可以确保您的结构体/联合成员都以正确的方式填充,则该操作是安全的,即使C++认为它是未定义的。
最后,从实用的角度来看,如果您不干扰填充并获得标准布局,则编译器通常会做正确的事情,因为这是C中的一种相当古老的使用模式,破坏这种模式将会破坏大量代码。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接