如何在不违反类型别名规则的情况下解释消息负载?

How do I interpret a message payload without violating type aliasing rules?

本文关键字:情况下 解释 消息 负载 规则 别名 类型      更新时间:2023-10-16

我的程序通过网络接收消息。这些消息由某些中间件反序列化(即我无法更改的其他人的代码)。我的程序接收如下所示的对象:

struct Message {
int msg_type;
std::vector<uint8_t> payload;
};

通过检查msg_type我可以确定消息有效负载实际上是,例如,一个uint16_t值数组。我想在没有不必要的副本的情况下读取该数组。

我的第一个想法是这样做:

const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());

但是,从a中读取似乎违反了标准。以下是条款 3.10.10:

如果程序尝试通过以下类型之一以外的 glvalue 访问对象的存储值,则行为是未定义的:

  • 对象的动态类型,
  • 对象的动态类型的 CV 合格版本,
  • 与对象的动态类型类似(如 4.4 中所定义)的类型,
  • 一种类型,该类型是对应于对象的动态类型的有符号或无符号类型,
  • 一种类型,
  • 该类型是与对象的动态类型的 CV 限定版本相对应的有符号或无符号类型,
  • 在其元素或非静态数据成员中包含上述类型之一的聚合或联合
  • 类型(递归地包括子聚合或包含的联合的元素或非静态数据成员),
  • 一种类型,该类型是对象的动态类型的基类类型(可能符合 CV 标准),
  • charunsigned char类型。

在这种情况下,a将是glvalue,uint16_t*似乎不符合任何列出的标准。

那么,如何在不调用未定义的行为或执行不必要的复制的情况下将有效负载视为uint16_t值数组呢?

如果你要一个接一个地使用这些值,那么你可以memcpy到一个uint16_t,或者写payload[0] + 0x100 * payload[1]等,关于你想要的行为。这不会是"低效的"。

如果你必须调用一个只接受uint16_t数组的函数,并且你不能改变提供Message的结构,那么你就不走运了。在标准C++中,您必须制作副本。

如果您使用的是 gcc 或 clang,另一种选择是在编译相关代码时设置-fno-strict-aliasing

如果你想严格遵循没有UB C++标准,并且不使用非标准的编译器扩展,你可以尝试:

uint16_t getMessageAt(const Message& msg, size_t i) {
uint16_t tmp;
memcpy(&tmp, msg.payload.data() + 2 * i, 2);
return tmp;
}

编译器优化应避免在生成的机器代码中memcpy复制;例如,请参阅类型双关语、严格别名和优化。

事实上,有复制到返回值中,但根据你将如何处理它,这个拷贝也可以被优化掉(例如,这个值可以加载到寄存器中并只在那里使用)。

如果你想严格正确,正如你引用的标准所说,你不能。 如果您希望明确定义行为,则需要进行复制。

如果代码是可移植的,则需要以任何一种方式处理字节序,并从单个uint8_t字节重建uint16_t值,根据定义,这需要一个副本。

如果你真的知道自己在做什么,你可以忽略标准,只做你描述的reinterpret_cast。

GCC 和 clang 支持-fno-strict-aliasing以防止优化生成损坏的代码。 据我所知,在撰写本文时,Visual Studio编译器没有标志,并且从不执行此类优化 - 除非您使用declspec(restrict)__restrict

您的代码可能不是 UB(或边界线,具体取决于读者的敏感性),例如,如果vector数据是这样构建的:

Message make_array_message(uint16_t* x, size_t n){
Message m;
m.type = types::uint16_t_array;
m.payload.reserve(sizeof(uint16_t)*n);
std::copy(x,x+n,reinterpret_cast<uint16_t*>(m.payload.data()));
return m;
}

在此代码中,向量的数据保存一个uint16_t序列,即使它被声明为uint8_t。因此,使用此指针访问数据:

const uint16_t* a = reinterpret_cast<uint16_t*>(msg.payload.data());

完全没问题。但是以uint8_t访问vector的数据将是UB。访问a[1]适用于所有编译器,但在当前标准中它是 UB。这可以说是标准中的一个缺陷,c++ 标准化委员会正在努力修复它,请参阅 P0593 用于低级对象操作的隐式对象创建。

截至目前,在我自己的代码中,我不处理标准中的缺陷,我更喜欢遵循编译器行为,因为对于这个主题,这是制定规则的编码者和编译器,标准只会遵循!

相关文章: