有许多情况(特别是在低级编程中),其中数据的二进制布局很重要。例如:硬件/驱动程序操作,网络协议等
在C ++中,我可以使用char*
和按位操作(掩码和移位)来读/写任意二进制结构,但这很乏味且容易出错。显然,我试图限制这些操作的范围并将它们封装在更高级别的API中,但它仍然很痛苦。
C++ bitfields似乎为这个问题提供了一个开发人员友好的解决方案,但遗憾的是他们的存储空间为implementation specific。
NathanOliver提到std::bitset
,它基本上允许您访问一个整数的单个位,其中包含一个很好的operator[]
但缺少多位字段的访问器。
使用元编程和/或宏,可以抽象库中的按位运算。由于我不想重新发明轮子,我正在寻找能够做到这一点的(最好是STL或者提升)库。
为了记录,我正在调查DNS解析器,但问题及其解决方案应该是通用的。
修改:简短回答:事实证明位域的存储在实践中是可靠的(即使它没有被标准规定)系统/网络库使用它们,并在使用主流编译器编译时表现良好的程序。
答案 0 :(得分:9)
从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;
};
答案 1 :(得分:5)
使用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
基本上你必须手工编写
答案 2 :(得分:3)
我们在生产代码中有这个,我们必须将MIPS代码移植到x86-64
适合我们。
它基本上是一个没有任何存储空间的模板,模板参数指定了相关位的位置。
如果您需要多个字段,可以将模板的多个特化项放在一个联合中,并与一个字节数组一起提供存储。
模板具有用于赋值的重载和转换运算符到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;
}
答案 3 :(得分:1)
我在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;
使用bit字段的这种实现,traits模板参数为程序员提供了很大的灵活性。内存默认只是处理器内存,或者它可以是抽象,程序员提供执行“内存”读写的功能。抽象存储器是任何无符号整数类型的元素序列(由程序员选择)。字段可以从最小到最大或最不重要的方面排列。内存中字段的布局可以与格式结构中的字段相反。
该实施位于:https://github.com/wkaras/C-plus-plus-library-bit-fields
(正如您所看到的,遗憾的是我无法完全避免使用宏。)
答案 4 :(得分:0)
C专为低级位操作而设计。声明一个无符号字符缓冲区并将其设置为您想要的任何位模式都很容易。特别是如果你的位串非常短,那么就适合其中一种整数类型。
一个潜在的问题是字节字节顺序。 C不能&#34;看&#34;这一点,但正如整数有一个字节序,字节序列化也是如此。另一个是非常少数的机器不使用八位字节作为字节。 C保证一个字节至少应为八位字节,但32和9是实际的实现。在这种情况下,你必须决定是否简单地忽略uper位(在这种情况下天真的代码应该工作),或者将它们视为比特流的一部分(在这种情况下你必须小心折叠) CHAR_BIT进入你的计算)。测试代码也很困难,因为你不太可能轻易找到CHAR + BIT 32机器。