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

时间:2018-08-02 11:22:10

标签: c++

我的程序通过网络接收消息。这些消息由某些中间件反序列化(即,我无法更改的其他人的代码)。我的程序收到的对象看起来像这样:

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值的数组?

4 个答案:

答案 0 :(得分:15)

如果您要逐个使用这些值,则可以memcpyuint16_t或写payload[0] + 0x100 * payload[1]等,以决定所需的行为。这不会是“低效”的。

如果您必须调用仅包含uint16_t数组的函数,并且无法更改提供Message的结构,那么您就不走运了。在Standard C ++中,您必须进行复制。

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

答案 1 :(得分:15)

如果要严格遵循不带UB的C ++ Standard,并且不采用非标准的编译器扩展,则可以尝试:

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

事实上,有复制到返回值中的方法,但是根据您将如何使用它,也可以优化此副本(例如,可以将该值加载到寄存器中并仅在其中使用)。

答案 2 :(得分:2)

如果您想要严格正确,如引用的标准所述,则不能。 如果您希望行为得到良好定义,则需要进行复制。

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

如果您真的知道自己在做什么,则可以忽略该标准,而只需执行您描述的reinterpret_cast。

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

答案 3 :(得分:-3)

例如,如果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());

很好。但是,以vector的身份访问uint8_t的数据将是UB。访问a[1]可以在所有编译器上使用,但在当前标准中为UB。可以说这是标准中的缺陷,而c ++标准化委员会正在努力对其进行修复,请参见P0593 Implicit object creation for low level object manipulation

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