【问题标题】:Portable solution to data alignment (Serialization)数据对齐的便携式解决方案(序列化)
【发布时间】:2014-12-08 15:41:12
【问题描述】:

假设我正在尝试从文件中读取数据。数据存储为二进制数据,可以使用编译器打包扩展轻松读取(为清楚起见,使用 C99 表示法):

#pragma pack(push, 1) /* visual studio */
struct S
{
  int16_t v1;
  uint32_t v2; /* gcc would use: __attribute__((packed)) */
  int16_t v3;
};
#pragma pack(pop)
void read( std::istream & is )
{
  S s;
  assert( sizeof(s) == 8 ); // packed !
  is.read( (char*)&s, sizeof(s) );
  std::cout << s.v1 << " " << s.v2 << " " << s.v3 << std::endl;
}

编写相同的代码但考虑到可移植性会变得更加混乱:

struct S2
{
  unsigned char buf1[2];
  unsigned char buf2[4];
  unsigned char buf3[2];
};
static inline uint16_t makenum(const unsigned char (&x)[2])
{
  return x[0] | (x[1] << 8);
}
static inline uint32_t makenum(const unsigned char (&x)[4])
{
  return
      ((uint32_t)x[0] <<  0)
    | ((uint32_t)x[1] <<  8)
    | ((uint32_t)x[2] << 16)
    | ((uint32_t)x[3] << 24);
}
void read( std::istream & is )
{
  S2 s2;
  assert( sizeof(s2) == 8 ); // garanteed !
  is.read( (char*)&s2, sizeof(s2) );
  std::cout << makenum(s2.buf1) << " " << makenum(s2.buf2) << " " << makenum(s.buf3) << std::endl;
}

还有什么(更聪明的)可以做的吗?我猜移位和按位包含 OR 应该不会对执行产生太大影响,但是我找不到使用 union 的通用解决方案来避免计算。例如:伪解决方案(不工作):

struct S3
{
  union { char buf1[2]; int16_t v1; } uv1;
  union { char buf2[4]; uint32_t v2; } uv2;
  union { char buf3[2]; int16_t v3; } uv3;
};

【问题讨论】:

  • 我怀疑是否有使用简单结构来解决您的问题(尤其是跨平台)的真正可移植解决方案。你应该看看boost::serializationgooble protocol buffers 来实现你想要的。
  • htonl()ntohl() 这样的函数呢?
  • @AtlasC1 数据对齐!= 字节序

标签: c++ cross-platform portability


【解决方案1】:

我强烈建议不要使用struct 将字段与协议进行 1:1 映射

其中一个原因是允许编译器在字段之间添加填充。

另一个是您希望结构对您的处理器有效。希望您将更频繁地操作结构中的数据,而不是使用它执行 I/O。

例如,给定 32 位处理器对于 32 位数字非常有效,但对于 16 位则没有那么有效。该协议需要 16 位整数。那么,您将结构中的字段映射为 16 位还是 32 位?

答案:在结构中使用 32 位字段并编写方法来与协议进行转换。例如,要从内存中加载 16 位变量,32 位处理器可能必须执行取指和移位,具体取决于 16 位在 32 位寄存器中的位置。如果结构域为32位,则不需要移位;因此效率更高。

此外,编写协议转换函数允许您在不更改结构的情况下处理 Big-Endian 与 Little-Endian 问题。

【讨论】:

  • 虽然我理解您的一般评论,但 C 编译器不会在 char buf[512]typedef struct { char buf1[512/2]; char buf2[512/2]; } S; 之间产生任何区别(发布模式),从长远来看,第二种语法更易于阅读和维护术语。
【解决方案2】:

这是我想出的解决方案:

#include <cstring>
#include <stdint.h>

template <typename T>
struct Fast;
template <>
struct Fast<uint16_t> {
  typedef uint_fast16_t Type;
};
template <>
struct Fast<int16_t> {
  typedef int_fast16_t Type;
};
template <>
struct Fast<uint32_t> {
  typedef uint_fast32_t Type;
};
template <>
struct Fast<int32_t> {
  typedef int_fast32_t Type;
};
template <>
struct Fast<uint64_t> {
  typedef uint_fast64_t Type;
};
template <>
struct Fast<int64_t> {
  typedef int_fast64_t Type;
};

template <typename T>
struct Helper {
  typedef typename Fast<T>::Type RetType;
  typedef char (VecType)[sizeof(T)];
  typedef union { VecType vec; T val; } UType;
};

template <typename T>
struct MakeNum {
  typedef typename Helper<T>::RetType RetType;
  typedef typename Helper<T>::UType UType;

  static RetType Get(const char (&x)[sizeof(T)]) {
    UType u;
    memcpy( u.vec, x, sizeof(T) );
    return u.val;
  }
};


#define AddField( type, name ) \
  char name ## _[sizeof(type)]; \
  typename MakeNum<type>::RetType \
    name () const { return MakeNum<type>::Get(name ## _); }

struct S
{
  AddField( uint16_t, name1 );
  AddField(  int32_t, name2 );
  AddField( uint16_t, name3 );
};

int main()
{
  S s = { 0, 1, 0, 1, 0, 0, 0, 1 };
  return s.name1() + s.name2() + s.name3();
}

它确实使用 gcc 4.9.1 生成与此(非便携式)完全相同的代码:

#include <stdint.h>

struct S2
{
  uint16_t v1;
   int32_t v2 __attribute__((packed));
  uint16_t v3;
};

int main()
{
  S2 u = { 256, 256, 256 };
  return u.v1 + u.v2 + u.v3;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-21
    • 2016-04-05
    • 1970-01-01
    • 2011-05-28
    • 2015-04-06
    • 1970-01-01
    • 2013-09-25
    • 2016-04-04
    相关资源
    最近更新 更多