【问题标题】:Is there a portable alternative to C++ bitfields是否有 C++ 位域的可移植替代品
【发布时间】:2015-10-21 22:53:02
【问题描述】:

在许多情况下(尤其是在低级编程中),数据的二进制布局很重要。例如:硬件/驱动程序操作、网络协议等。

在 C++ 中,我可以使用 char* 和按位运算(掩码和移位)读取/写入任意二进制结构,但这很乏味且容易出错。显然,我尝试限制这些操作的范围,并将它们封装在更高级别的 API 中,但这仍然很痛苦。

C++ bitfields 似乎为这个问题提供了一个开发人员友好的解决方案,但不幸的是他们的存储是implementation specific

NathanOliver 提到了std::bitset,它基本上允许您使用漂亮的operator[] 访问整数的各个位,但缺少多位字段的访问器。

使用元编程和/或宏,可以在库中抽象按位操作。因为我不想重新发明轮子,所以我正在寻找一个(最好是 STL 或 boost)库来做到这一点。

为了记录,我正在寻找 DNS 解析器,但问题及其解决方案应该是通用的。

编辑:简短回答:事实证明,bitfield 的存储在实践中是可靠的(即使标准没有强制要求),因为系统/网络库使用它们并产生使用主流编译器编译时表现良好的程序。

【问题讨论】:

  • 有时我通过在编译时使用一些元编程抽象位操作来解决这个问题。
  • 你看过std::bitset
  • 本想回答这个问题,但它被关闭了,也许我应该写一篇博客文章,介绍我在我从事的嵌入式项目中是如何做到这一点的。
  • 离题?从什么时候开始,“我该如何做得更好”的问题离题了?
  • @NathanOliver(和其他人)(恕我直言)收到此 VTC 原因的问题通常是非常广泛的问题(“我在哪里学习 X?”),无法以独立的方式回答.这个问题非常集中,可以用临时代码、标准功能、Boost 或其他库来回答。考虑到手头相对较小的问题,我会说可能有独立的临时答案,这很好。事实上,我有一个这样的答案。

标签: c++ bit-fields


【解决方案1】:

来自 C++14 标准(N3797 草案),第 9.6 节 [class.bit],第 1 段:

类对象内的位域分配是实现定义的。 位域的对齐是实现定义的。位域被打包到一些可寻址的分配单元中。 [注意:位域在某些机器上跨越分配单元,而不在其他机器上。位域在某些机器上从右到左分配,在其他机器上从左到右分配。 ——尾注]

虽然注释是非规范性的,但我知道的每个实现都使用两种布局之一:大端或小端位顺序。

注意:

  • 您必须手动指定填充。这意味着您必须知道类型的大小(例如,使用 <cstdint>)。
  • 您必须使用无符号类型。
  • 用于检测位顺序的预处理器宏依赖于实现。
  • 通常位顺序字节序与字节顺序字节序相同。我相信有一个编译器标志可以覆盖它,但我找不到它。

例如,查看netinet/tcp.h 和其他附近的标头。

由 OP 编辑​​:例如 tcp.h 定义

struct
{
    u_int16_t th_sport;     /* source port */
    u_int16_t th_dport;     /* destination port */
    tcp_seq th_seq;     /* sequence number */
    tcp_seq th_ack;     /* acknowledgement number */
# if __BYTE_ORDER == __LITTLE_ENDIAN
    u_int8_t th_x2:4;       /* (unused) */
    u_int8_t th_off:4;      /* data offset */
# endif
# if __BYTE_ORDER == __BIG_ENDIAN
    u_int8_t th_off:4;      /* data offset */
    u_int8_t th_x2:4;       /* (unused) */
# endif
    // ...
}

而且由于它适用于主流编译器,这意味着 bitset 的内存布局在实践中是可靠的。

编辑:

这是在一个字节序内可移植的:

struct Foo {
    uint16_t x: 10;
    uint16_t y: 6;
};

但这可能不是因为它跨越了一个 16 位单元:

struct Foo {
    uint16_t x: 10;
    uint16_t y: 12;
    uint16_t z: 10;
};

这可能不是因为它有隐式填充:

struct Foo {
    uint16_t x: 10;
};

【讨论】:

  • 你是说所有的主流编译器都将位域打包得很紧(并且域可能跨越字节),优化级别对位域的二进制布局没有影响?您是否检查过自己,或者您可以链接到一些指向您的方式的信息?
  • 是的,前提是您不跨越单元边界并填充到该大小。如果优化影响结构布局,整个宇宙都会燃烧。
  • 确实,tcp.h 标头说服了我,实际上位域是可用的,即使在 SO 上有很多关于它们过于谨慎的帖子。
  • 我相信标准是在谈论位域的顺序而不是位的顺序。
  • @HCSF All 实现选择超出标准要求的附加限制。这是您可以信赖的事情之一。
【解决方案2】:

用 C++ 实现具有已知位置的位域很简单:

template<typename T, int POS, int SIZE>
struct BitField {
    T *data;

    BitField(T *data) : data(data) {}

    operator int() const {
        return ((*data) >> POS) & ((1ULL << SIZE)-1);
    }

    BitField& operator=(int x) {
        T mask( ((1ULL << SIZE)-1) << POS );
        *data = (*data & ~mask) | ((x << POS) & mask);
        return *this;
    }
};

上面的玩具实现例如允许在unsigned long long 变量中定义一个 12 位字段

unsigned long long var;

BitField<unsigned long long, 7, 12> muxno(&var);

而生成的访问字段值的代码就是

0000000000000020 <_Z6getMuxv>:
  20:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax  ; Get &var
  27:   48 8b 00                mov    (%rax),%rax     ; Get content
  2a:   48 c1 e8 07             shr    $0x7,%rax       ; >> 7
  2e:   25 ff 0f 00 00          and    $0xfff,%eax     ; keep 12 bits
  33:   c3                      retq   

基本上你必须手写的内容

【讨论】:

  • 您可以通过将它们放在一个联合中来避免指针/内存开销。此外,您应该在某些地方使用size_tT 而不是int,以及`static_assert(std::is_unsigned::value, "bitfields must be unsigned to be sane")。
【解决方案3】:

我在 C++ 中编写了一个位字段的实现作为库头文件。我在文档中给出的一个例子是,而不是这样写:

struct A
  {
    union
      {
        struct
          {
            unsigned x : 5;
            unsigned a0 : 2;
            unsigned a1 : 2;
            unsigned a2 : 2;
          }
        u;
        struct
          {
            unsigned x : 5;
            unsigned all_a : 6;
          }
        v;
      };
  };

// …

A x;
x.v.all_a = 0x3f;
x.u.a1 = 0;

你可以写:

typedef Bitfield<Bitfield_traits_default<> > Bf;

struct A : private Bitfield_fmt
  {
    F<5> x;
    F<2> a[3];
  };

typedef Bitfield_w_fmt<Bf, A> Bwf;

// …

Bwf::Format::Define::T x;
BITF(Bwf, x, a) = 0x3f;
BITF(Bwf, x, a[1]) = 0;

还有一个替代界面,在这个界面下上面的最后两行会变成:

#define BITF_U_X_BWF Bwf
#define BITF_U_X_BASE x
BITF(X, a) = 0x3f;
BITF(X, a[1]) = 0;

使用位域的这种实现,traits 模板参数为程序员提供了很大的灵活性。默认情况下,内存只是处理器内存,或者它可以是一种抽象,程序员提供函数来执行“内存”读取和写入。抽象内存是任何无符号整数类型的元素序列(由程序员选择)。字段可以按重要性从最低到最高或从最高到最低进行排列。内存中字段的布局可以与它们在格式结构中的布局相反。

实现位于:https://github.com/wkaras/C-plus-plus-library-bit-fields

(如您所见,很遗憾,我无法完全避免使用宏。)

【讨论】:

    【解决方案4】:

    C 是为低级位操作而设计的。很容易声明一个无符号字符的缓冲区,并将其设置为您想要的任何位模式。特别是如果您的位字符串非常短,因此适合其中一种整数类型。

    一个潜在的问题是字节字节序。 C 根本无法“看到”这一点,但正如整数具有字节顺序一样,字节在序列化时也是如此。另一个是极少数不使用八位字节的机器。 C 保证一个字节至少是一个八位字节,但 32 和 9 是真实世界的实现。在这种情况下,您必须决定是简单地忽略高位(在这种情况下,幼稚的代码应该可以工作),还是将它们视为比特流的一部分(在这种情况下,您必须小心将 CHAR_BIT 折叠成你的计算)。测试代码也很困难,因为您不太可能很容易接触到 CHAR+BIT 32 机器。

    【讨论】:

      【解决方案5】:

      我们在生产代码中有这个,我们必须将 MIPS 代码移植到 x86-64

      https://codereview.stackexchange.com/questions/54342/template-for-endianness-free-code-data-always-packed-as-big-endian

      对我们很有效。

      它基本上是一个没有任何存储的模板,模板参数指定相关位的位置。

      如果您需要多个字段,请将模板的多个特化放在一个联合中,并与一个字节数组一起提供存储。

      模板具有用于赋值的重载和用于读取值的unsigned 的转换运算符。

      此外,如果字段大于一个字节,则它们以大端字节顺序存储,这在实现跨平台协议时有时很有用。

      这是一个用法示例:

      union header
      {
          unsigned char arr[2];       // space allocation, 2 bytes (16 bits)
      
          BitFieldMember<0, 4> m1;     // first 4 bits
          BitFieldMember<4, 5> m2;     // The following 5 bits
          BitFieldMember<9, 6> m3;     // The following 6 bits, total 16 bits
      };
      
      int main()
      {
          header a;
          memset(a.arr, 0, sizeof(a.arr));
          a.m1 = rand();
          a.m3 = a.m1;
          a.m2 = ~a.m1;
          return 0;
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-10-14
        • 2023-04-02
        • 2017-03-11
        • 2015-10-18
        • 1970-01-01
        • 2017-03-25
        • 1970-01-01
        • 2013-01-28
        相关资源
        最近更新 更多