2成员结构是否可以安全替换位紧凑的int?

时间:2019-05-20 18:51:40

标签: c++ struct memory-alignment

我有一些现有的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
}

但是,这很丑陋,类型不安全,并且依赖于硬编码的幻数。我想知道是否有可能通过定义2个成员结构来实现相同的目的,即“应该”与uint32_t的大小完全相同:

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_t的位吗?换句话说,使用struct data_item_t时和使用uint32_t并进行位打包时,items数组将包含相同的位吗?基于the rules of structure padding,我认为包含两个uint16_t的结构永远不需要任何内部填充即可正确对齐。还是实际上取决于我的编译器,并且我需要像__attribute__((__packed__))这样的东西来保证它?

3 个答案:

答案 0 :(得分:3)

不应存在任何实现定义的填充问题,但是根据字节顺序, 之间会有所不同。还要注意对齐方式会有所不同-例如,如果您将值嵌入到另一个结构中,则对齐变得很重要。

更一般地说,您不清楚要达到什么级别的协议兼容性。我建议您要么决定要允许版本之间的协议兼容性中断,要么非常明确地以可扩展和版本化的方式放下协议,以使软件的不同版本可以通信。在这种情况下,您应该设计协议,以便独立于C ++实现对其进行良好定义,并以字节为单位编写发送/接收代码,以避免字节顺序问题。

我看不出在更改表示形式时试图保持相等数据大小的实现方式。

答案 1 :(得分:0)

  

这很丑陋,类型不安全,并且依赖于硬编码的幻数。

这是一个众所周知的习惯用法,这是自C以来我们获得位操作运算符的原因之一。这些数字中没有“魔术”。

另一种选择是,只要您知道自己的字节序,就可以致电std::memcpy。如果您担心的话,这也更容易概括。

  

我想知道是否可以通过定义2个成员结构来实现相同的目的,即“应该”与uint32_t的大小完全相同。

不是2成员结构,但是您可以使用2个uint16_t数组来做,这将确保它们之间没有填充。

您也可以根据需要使用2个成员,但是断言该大小是最小的。至少以这种方式,可以确保它在编译后仍能正常工作(在当今的大多数平台上都可以):

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

这项工作等效于位uint32_t的位吗?换句话说,当我使用struct data_item_t时,和使用uint32_t并进行位打包时,items数组包含相同的位吗?

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

  

或者这实际上取决于我的编译器,我需要像__attribute__((__packed__))这样的东西来保证它吗?

那是该属性的存在理由(特别是对于不同的类型)。 :-)

答案 2 :(得分:0)

只需编写适当的访问器:

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类。因此,通过使用指向charunsigned charstd::byte的指针的api接收它是有效的。

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