繁体   English   中英

是否有 C++ 位域的可移植替代品

[英]Is there a portable alternative to C++ bitfields

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

在 C++ 中,我可以使用char*和按位运算(掩码和移位)读/写任意二进制结构,但这很乏味且容易出错。 显然,我尝试着把这些操作限制在scope,封装在更高层的API中,但还是很痛苦。

C++ 位域似乎为这个问题提供了一个开发人员友好的解决方案,但不幸的是它们的存储是特定于实现的

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

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

作为记录,我正在研究这个DNS解析器,但问题及其解决方案应该是通用的。

编辑:简短回答:事实证明位域的存储在实践中是可靠的(即使它不是标准强制要求的)因为 system.network 库使用它们并且在使用主流编译器编译时产生表现良好的程序。

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

类对象中位域的分配是实现定义的。 位字段的对齐是实现定义的。 比特字段被打包到一些可寻址的分配单元中。 [注意:位字段跨越某些机器上的分配单元而不是其他机器上的分配单元。 在某些机器上从右到左分配位字段,在其他机器上从左到右分配。 - 结束说明]

虽然注释是非规范的,但我所知道的每个实现都使用两种布局之一:big-endian或little endian位顺序。

注意:

  • 您必须手动指定填充。 这意味着您必须知道类型的大小(例如,使用<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;
};

使用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   

基本上你必须手工编写

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

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

适合我们。

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

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

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

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

这是一个用法示例:

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;
}

我已经在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

(如你所见,遗憾的是我无法完全避免使用宏。)

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

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

我为此创建了一个库:

便携式位域

它的工作原理类似于@CpusPuzzle提供的解决方案。

基本示例:

enum class Id
{
    f1, f2, f3
};

using namespace jungles;

using Register = Bitfields<
    uint16_t, 
    Field{.id = Id::f1, .size = 3},
    Field{.id = Id::f2, .size = 9}, 
    Field{.id = Id::f3, .size = 4}>; 

r.at<Id::f1>() = 0b101;
r.at<Id::f2>() = 0b001111100;
r.at<Id::f3>() = 0b0110;

ASSERT(r.extract<Id::f1>() == 0b1010000000000000);
ASSERT(r.extract<Id::f2>() == 0b0000011111000000);
ASSERT(r.extract<Id::f3>() == 0b0000000000000110);

ASSERT(r.serialize() == 0b1010011111000110);

反序列化:

Register r{0b0101110001110110};
//           XXXYYYYYYYYYZZZZ
ASSERT(r.at<Id::f1>() == 0b010);
ASSERT(r.at<Id::f2>() == 0b111000111);
ASSERT(r.at<Id::f3>() == 0b0110);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM