最语义正确和类型安全的构造从序列化字节数组?(c++ 11)



考虑下面的c++11类,它代表一个IPv4头结构,无论字节顺序如何,都可以从字节数组中构造。

#include <arpa/inet.h>
#include <netinet/in.h>
namespace Net {
  using addr_t = ::in_addr_t;
  #pragma pack(push, 1)
  struct ip_header_t {
    uint8_t  ver_ihl;
    uint8_t  tos;
    uint16_t total_length;
    uint16_t id;
    uint16_t flags_fo;
    uint8_t  ttl;
    uint8_t  protocol;
    uint16_t checksum;
    addr_t   src_addr;
    addr_t   dst_addr;
    ip_header_t( const uint8_t* bytes, const bool ntoh = false ) {
      auto o = (ip_header_t&)*bytes;
      ver_ihl      = o.ver_ihl;
      tos          = o.tos;
      ttl          = o.ttl;
      protocol     = o.protocol;
      total_length = ntoh? ntohs(o.total_length) : o.total_length;
      id           = ntoh? ntohs(o.id) : o.id;
      flags_fo     = ntoh? ntohs(o.flags_fo) : o.flags_fo;
      checksum     = ntoh? ntohs(o.checksum) : o.checksum;
      src_addr     = ntoh? ntohl(o.src_addr) : o.src_addr;
      dst_addr     = ntoh? ntohl(o.dst_addr) : o.dst_addr;
    };
  };
  #pragma pack(pop)
}

我担心接受字节数组可能不是最安全或最语义正确的方法来做到这一点。将数组强制转换为结构本身似乎是一种非常像c语言的方法,缺乏类型安全性(更不用说边界检查了)。要求调用者担心这一点并要求对实例的const引用会更好吗?

将字节数组强制转换为这个类绝对不是正确的事情-正如您提到的,字节顺序可能在不同的系统上不同(这就是为什么在构造函数中有一个ntohs)。

把类放在哪里完全取决于实体的角色和职责。

表示原始二进制数据的类型,具有某种假定的布局:

template<typename T, size_t order>
struct serial_tag {};

引入一些名称,表示磁盘上数据的预期类型和布局:

typedef serial_tag<uint8_t , 0> ver_ihl_ser;
typedef serial_tag<uint8_t , 1> tos_ser;
typedef serial_tag<uint16_t, 2> total_length_ser;
    ...
typedef serial_tag<addr_t  , 9> dst_addr_ser;

一组serial_tags,可以被其他代码操作:

template<typename... tags>
struct serial_pack {};

编写接受serial_pack的代码,并确保每个序数都在使用中,没有间隔。

编写接受反序列化迭代器和serial_tag的代码,并在从serial_tag生成数据的同时推进反序列化迭代器。这应该处理端序

目标是以一种适合元编程的方式描述数据的原始数据布局,然后使用此布局信息将数据加载到c++结构体中。

这是一个流读操作,反序列化迭代器(或range)知道它的大小是否有限制,并且知道你是否正确地按顺序读取元素(至少在调试中)。

我不知道这是否值得,但它确实解决了你的问题。

这种方法的缺点是它违反了DRY,因为在理论上,内存布局可以用作描述序列化后的原始字节布局的方法。相反,我们必须维护一组完全不同的数据来表示它。作为一个有利的方面,这意味着我们在c++中的布局不必完全复制二进制布局。

在我看来,最好的解决方案是提供一个复制构造函数,它可以处理字节顺序转换,并依赖调用者进行强制转换。

一样:

/* copy constructor: */
ip_header_t( const ip_header_t& src, const bool ntoh = false )
  : ver_ihl(src.ver_ihl),
    tos(src.tos),
    ttl(src.ttl),
    protocol(src.protocol) {
  total_length = ntoh? ntohs(src.total_length) : src.total_length;
  id           = ntoh? ntohs(src.id)           : src.id;
  flags_fo     = ntoh? ntohs(src.flags_fo)     : src.flags_fo;
  checksum     = ntoh? ntohs(src.checksum)     : src.checksum;
  src_addr     = ntoh? ntohl(src.src_addr)     : src.src_addr;
  dst_addr     = ntoh? ntohl(src.dst_addr)     : src.dst_addr;
};
/* client code using byte array in network-order */
auto ip_header = Net::ip_header_t((Net::ip_header_t&)*(byte_array), true);

我也不能100%肯定我最喜欢这个解决方案。考虑到字节顺序交换与对象的构造没有严格的关系,创建一个非成员函数可能会更好。类也不应该关心字段的对齐和排序。

最新更新