是否有 C++ 位域的可移植替代方案
Is there a portable alternative to C++ bitfields
在很多情况下(尤其是在低级编程中),数据的二进制布局很重要。例如:hardware/driver操纵、网络协议等
在 C++ 中,我可以使用 char*
和按位运算(掩码和移位)read/write 任意二进制结构,但这很乏味且容易出错。显然,我试图限制这些操作的范围,将它们封装在更高级别的API中,但仍然很痛苦。
C++ bitfields seem to offer a developer-friendly solution to this problem, but unfortunately their storage is implementation specific.
NathanOliver 提到了 std::bitset
,它基本上允许您使用漂亮的 operator[]
访问整数的各个位,但缺少多位字段的访问器。
使用元编程 and/or 宏,可以在库中抽象出按位运算。因为我不想重新发明轮子,所以我正在寻找一个(最好是 STL 或 boost)库来做到这一点。
郑重声明,我正在研究这个 DNS 解析器,但问题及其解决方案应该是通用的。
编辑:简答:事实证明位域的存储在实践中是可靠的(即使它不是标准强制要求的)因为system/network 库使用它们并在使用主流编译器编译时生成性能良好的程序。
来自 C++14 标准(N3797 草案),第 9.6 节 [class.bit],第 1 段:
Allocation of bit-fields within a class object is implementation-defined.
Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit.
[ Note: Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note ]
虽然注释是 non-normative,但我知道的每个实现都使用两种布局之一:big-endian 或小端位顺序。
注意:
- 您必须手动指定填充。这意味着您必须知道类型的大小(例如,通过使用
<cstdint>
)。
- 您必须使用无符号类型。
- 用于检测位序的预处理器宏是implementation-dependent。
- 通常位序字节序与字节序字节序相同。不过,我相信有一个编译器标志可以覆盖它,但我找不到它。
例如,查看 netinet/tcp.h
和附近的 headers。
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 [=12=]x7,%rax ; >> 7
2e: 25 ff 0f 00 00 and [=12=]xfff,%eax ; keep 12 bits
33: c3 retq
基本上你必须手写的东西
我已经用 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 模板参数为程序员提供了很大的灵活性。默认情况下,内存只是处理器内存,或者它可以是一个抽象,程序员提供函数来执行 "memory" 读取和写入。抽象内存是任意无符号整数类型(由程序员选择)的元素序列。字段可以从最低到最高或最高到最低的重要性进行布局。字段在内存中的布局可以与它们在格式结构中的布局相反。
实现位于:https://github.com/wkaras/C-plus-plus-library-bit-fields
(如您所见,很遗憾,我无法完全避免使用宏。)
C 专为低级位操作而设计。很容易声明一个无符号字符的缓冲区,并将其设置为您想要的任何位模式。特别是如果您的位串非常短,那么适合其中一种整数类型。
一个潜在的问题是字节序。 C 根本无法"see",但正如整数具有字节序一样,序列化时字节也是如此。另一个是极少数机器不使用八位字节作为字节。 C 保证一个字节至少是一个八位字节,但 32 和 9 是现实世界的实现。在这种情况下,您必须决定是简单地忽略高位(在这种情况下,原始代码应该可以工作),还是将它们视为比特流的一部分(在这种情况下,您必须小心折叠 CHAR_BIT 到你的计算中)。测试代码也很困难,因为您不太可能会发现很容易上手使用 CHAR+BIT 32 机器。
我们在生产代码中有这个,我们必须将 MIPS 代码移植到 x86-64
对我们来说效果很好。
它基本上是一个没有任何存储的模板,模板参数指定相关位的位置。
如果您需要多个字段,您可以将模板的多个特化放在一个联合中,连同字节数组一起提供存储。
模板具有用于赋值的重载和用于读取值的 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;
}
我为此创建了一个库:
它的工作原理类似于 @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);
在很多情况下(尤其是在低级编程中),数据的二进制布局很重要。例如:hardware/driver操纵、网络协议等
在 C++ 中,我可以使用 char*
和按位运算(掩码和移位)read/write 任意二进制结构,但这很乏味且容易出错。显然,我试图限制这些操作的范围,将它们封装在更高级别的API中,但仍然很痛苦。
C++ bitfields seem to offer a developer-friendly solution to this problem, but unfortunately their storage is implementation specific.
NathanOliver 提到了 std::bitset
,它基本上允许您使用漂亮的 operator[]
访问整数的各个位,但缺少多位字段的访问器。
使用元编程 and/or 宏,可以在库中抽象出按位运算。因为我不想重新发明轮子,所以我正在寻找一个(最好是 STL 或 boost)库来做到这一点。
郑重声明,我正在研究这个 DNS 解析器,但问题及其解决方案应该是通用的。
编辑:简答:事实证明位域的存储在实践中是可靠的(即使它不是标准强制要求的)因为system/network 库使用它们并在使用主流编译器编译时生成性能良好的程序。
来自 C++14 标准(N3797 草案),第 9.6 节 [class.bit],第 1 段:
Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit. [ Note: Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others. — end note ]
虽然注释是 non-normative,但我知道的每个实现都使用两种布局之一:big-endian 或小端位顺序。
注意:
- 您必须手动指定填充。这意味着您必须知道类型的大小(例如,通过使用
<cstdint>
)。 - 您必须使用无符号类型。
- 用于检测位序的预处理器宏是implementation-dependent。
- 通常位序字节序与字节序字节序相同。不过,我相信有一个编译器标志可以覆盖它,但我找不到它。
例如,查看 netinet/tcp.h
和附近的 headers。
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 [=12=]x7,%rax ; >> 7
2e: 25 ff 0f 00 00 and [=12=]xfff,%eax ; keep 12 bits
33: c3 retq
基本上你必须手写的东西
我已经用 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 模板参数为程序员提供了很大的灵活性。默认情况下,内存只是处理器内存,或者它可以是一个抽象,程序员提供函数来执行 "memory" 读取和写入。抽象内存是任意无符号整数类型(由程序员选择)的元素序列。字段可以从最低到最高或最高到最低的重要性进行布局。字段在内存中的布局可以与它们在格式结构中的布局相反。
实现位于:https://github.com/wkaras/C-plus-plus-library-bit-fields
(如您所见,很遗憾,我无法完全避免使用宏。)
C 专为低级位操作而设计。很容易声明一个无符号字符的缓冲区,并将其设置为您想要的任何位模式。特别是如果您的位串非常短,那么适合其中一种整数类型。
一个潜在的问题是字节序。 C 根本无法"see",但正如整数具有字节序一样,序列化时字节也是如此。另一个是极少数机器不使用八位字节作为字节。 C 保证一个字节至少是一个八位字节,但 32 和 9 是现实世界的实现。在这种情况下,您必须决定是简单地忽略高位(在这种情况下,原始代码应该可以工作),还是将它们视为比特流的一部分(在这种情况下,您必须小心折叠 CHAR_BIT 到你的计算中)。测试代码也很困难,因为您不太可能会发现很容易上手使用 CHAR+BIT 32 机器。
我们在生产代码中有这个,我们必须将 MIPS 代码移植到 x86-64
对我们来说效果很好。
它基本上是一个没有任何存储的模板,模板参数指定相关位的位置。
如果您需要多个字段,您可以将模板的多个特化放在一个联合中,连同字节数组一起提供存储。
模板具有用于赋值的重载和用于读取值的 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;
}
我为此创建了一个库:
它的工作原理类似于 @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);