【问题标题】:How do I interpret a message payload without violating type aliasing rules?如何在不违反类型别名规则的情况下解释消息负载?
【发布时间】:2018-08-02 11:22:10
【问题描述】:

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

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 条:

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

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

在这种情况下,a 将是泛左值,uint16_t* 似乎不符合任何列出的条件。

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

【问题讨论】:

  • 你不需要处理字节顺序吗?
  • 这将改善问题,以准确显示消息的内容,而不是“类似”。 (我的回答假设完全一样)
  • 提示:你如何确保payload.data()满足uint16_t[]的对齐要求?
  • @M.M 我不同意:这是一个相对常见的问题——至少,我在不同的项目中遇到过多次——所以我抽象了问题的本质。提供其中一种消息类型的完整副本只会掩盖这一点。
  • @Jarod42 根据字节序提供两种不同的选项是有意义的:一个带有字节序不匹配的副本;以及它们匹配的非复制选项。至少,如果这个问题的答案不只是“你不走运”,它会这样。

标签: c++


【解决方案1】:

如果您要一个一个地使用这些值,那么您可以memcpyuint16_t,或写payload[0] + 0x100 * payload[1] 等,至于您想要哪种行为。这不会是“低效的”。

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

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

【讨论】:

  • @bolov launder 没有绕过严格的别名规则。如果您通过恶作剧获得了指针,它仅声明该对象可通过指针访问。 Related question
  • 谢谢。你的第二段——具体来说,“在标准 C++ 中,你必须制作副本”——是我想知道的。
  • @ThomasMatthews 但是可以检查字节顺序和对齐方式。如果指针正确对齐并且字节序匹配,则无复制选项是有意义的。
  • 有一个提案(尚未接受)允许零拷贝版本工作:Implicit creation of objects for low-level object manipulation
  • @SJL 您链接到旧版本,使用this format 获取最新版本。据我了解,reinterpret_cast 只能用于new'd 尚未“印记”任何内容的字符块;它不能用于按值传递的 vector 字符。因此代码必须是 uint16_t *p = std::launder(payload.data()); for (int i = 0; i &lt; payload.size() / 2; ++i) std::bless( p + i ); ,这是很多“什么都不做”的样板。我想这个提议会有更多的迭代
【解决方案2】:

如果你想在没有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在生成的机器码中复制到这里;参见,例如,Type Punning, Strict Aliasing, and Optimization

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

【讨论】:

  • uint16_t 返回类型是拼写错误还是故意的? OP 说的是uint16_t *
  • @Paula_plus_plus OP 想要读取 uint16_t 值,他们提出的方法是通过创建指针并取消引用来读取值。此答案通过不同的方法读取uint16_t 值,即调用此函数来读取每个值。
  • 哦!我知道了!感谢您提供详细信息!
  • 在 C++20 中,std::bit_cast 应该可以完成这项工作。
  • 谢谢。正如您所建议的那样,我熟悉将 memcpy 用于单个值,但我真的想在不复制的情况下对整个数组进行操作。
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

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

    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 Implicit object creation for low level object manipulation

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

    【讨论】:

    • 1) copy 并没有像你认为的那样做。 2) 您无法保证msg.payload.data() 正确对齐。 3)Message void.
    • 这与使用 aligned_storage 的“答案”本质上没有什么不同,您只是在向量末尾使用非对象存储。即使使用 P0593,我也不确定return m; 是否能保证保留您在那里写入的数据。 (你当然不会在任何阶段向向量中添加元素)
    • @M.M 啊,我错过了reserve。那完全坏了。
    • 即使你把reserve改成resize,这仍然是错误的。向量的数据将保存uint16_t 序列的字节,但那里没有uint16_t 对象,因此通过uint16_t 访问它的左值是UB。
    • @T.C.哈哈!! 1)我知道!!复制将调用 memcpy。 2) msg​​.payload.data() 将至少与 max_align_t 一样对齐,3) 固定
    猜你喜欢
    • 1970-01-01
    • 2015-12-30
    • 1970-01-01
    • 2015-11-06
    • 1970-01-01
    • 2019-09-16
    • 1970-01-01
    • 2017-06-15
    • 2011-08-20
    相关资源
    最近更新 更多