2 成员结构是否可以安全地替代位压缩 int?

Is a 2-member struct a safe replacement for a bit-packed int?

我有一些现有的 C++ 代码可以通过网络发送和接收一个 uint32_t 的数组。由于我的协议发生了变化,我想用一对两个 uint16_t 替换这个数组中的每个条目,如果可能的话,我想这样做而不改变我通过网络发送的位数。将两个 uint16_t 值组合成一个 32 位宽值的明显方法是将低级位打包成 uint32_t,并保持数组定义不变。所以发件人的代码将如下所示:

uint32_t items[ARR_SIZE];
for(std::size_t i = 0; i < ARR_SIZE; ++i) {
    //get uint16_t field1 and field2 somehow
    items[i] = static_cast<uint32_t>(static_cast<uint32_t>(field2) << 16)
                   | static_cast<uint32_t>(field1));
}

接收者的代码如下所示:

//receive items
for(std::size_t i = 0; i < ARR_SIZE; ++i) {
    uint16_t field1 = static_cast<uint16_t>(items[i] & 0xffff);
    uint16_t field2 = static_cast<uint16_t>(items[i] >> 16);
    //do something with field1 and field2
}

但是,这很丑陋,类型不安全,并且依赖于硬编码的幻数。我想知道是否可以通过定义一个 "should" 与 uint32_t:

大小完全相同的 2 成员结构来完成同样的事情
struct data_item_t {
    uint16_t field1;
    uint16_t field2;
};

那么,发件人的代码将如下所示:

data_item_t items[ARR_SIZE];
for(std::size_t i = 0; i < SIZE; ++i) {
    //get uint16_t field1 and field2 somehow
    items[i] = {field1, field2};
}

接收者的代码如下所示:

//receive items
for(std::size_t i = 0; i < ARR_SIZE; ++i) {
    uint16_t curr_field1 = items[i].field1;
    uint16_t curr_field2 = items[i].field2;
    //do something with field1 and field2
}

这是否与位压缩 uint32_ts 等效?换句话说,当我使用 struct data_item_t 时,项目数组包含的位是否与我使用 uint32_t 和位打包时相同?基于 the rules of structure padding,我认为包含两个 uint16_t 的结构永远不需要任何内部填充来正确对齐。或者这实际上取决于我的编译器,我需要像 __attribute__((__packed__)) 这样的东西来保证它?

不应该有任何 implementation-defined 填充的问题,但是根据字节顺序, 表示之间会有差异。另请注意,对齐方式会有所不同 - 例如,如果您将值嵌入到另一个结构中,这就会变得相关。

更一般地说,不清楚您试图达到何种级别的协议兼容性。我建议您要么决定允许打破版本之间的协议兼容性,要么以可扩展和版本化的方式非常明确地制定协议,以便不同版本的软件可以进行通信。在这种情况下,您应该设计协议,使其 well-defined 独立于您的 C++ 实现,并以 byte-by-byte 风格编写 send/receive 代码以避免字节顺序问题。

我完全看不出在改变表示法的同时保持相同的数据大小有什么效果。

This is ugly, type-unsafe, and relies on hard-coded magic numbers.

这是一个 well-known 习语,它是我们从 C 开始得到位操作运算符的原因之一。那些数字中没有 "magic"。

另一种选择是调用 std::memcpy,只要您知道字节序。如果您担心的话,这也更容易概括。

I wonder if it is possible to accomplish the same thing by defining a 2-member struct that "should" be exactly the same size as a uint32_t.

不使用 2 成员结构,但您可以使用 2 uint16_t 的数组来实现——这将保证它们之间没有填充。

您也可以根据需要使用 2 成员,但断言大小是最小值。至少这样你可以保证它在编译时会工作(现在在大多数平台上都会):

static_assert(sizeof(T) == 2 * sizeof(std::uint16_t));

Will this work equivalently to the bit-packed uint32_ts? In other words, will the items array contain the same bits when I use struct data_item_t as when I use uint32_t and bit-packing?

不,编译器可能会添加填充。

Or is that actually up to my compiler, and I need something like __attribute__((__packed__)) to guarantee it?

这就是该属性存在的理由(尤其是对于不同的类型)。 :-)

只需编写适当的访问器:

struct data_item_t {
    uint32_t field;
    uint16_t get_field1() const { return field; }
    uint16_t get_field2() const { return field >> 16; }
    void set_field1(uint16_t v) { field = (field & 0xffff0000) | v; }
    void set_field2(uint16_t v) { field = (field & 0x0000ffff) | v << 16; }
};
static_assert(std::is_trivially_copyable<data_item_t>::value == true, "");
static_assert(sizeof(data_item_t) == sizeof(uint32_t), "");
static_assert(alignof(data_item_t) == alignof(uint32_t), "");

is_trivially_copyable 就位了,memcpymemmove class 随心所欲。因此,通过一些使用指向 charunsigned charstd::byte 的指针的 api 接收它是有效的。

编译器可以在第一个成员前面以外的任何地方插入填充。所以即使只有一个字段,它也可以在结构的末尾插入填充——我们可能会找到一个奇怪的实现,其中 sizeof(data_item_t) == sizeof(uint64_t)。正确的做法是编写正确的 static_assertions。