Является ли 2-членная структура безопасной заменой для бит-упакованного int? - PullRequest
4 голосов
/ 20 мая 2019

У меня есть некоторый существующий код 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
}

Однако, это уродливо, небезопасно и зависит от жестко закодированных магических чисел. Интересно, возможно ли сделать то же самое, определив двухэлементную структуру, которая «должна» быть точно такого же размера, как 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 и упаковку битов? Основываясь на правилах заполнения структуры , я думаю, что структура, содержащая два uint16_t s, никогда не будет нуждаться в каком-либо внутреннем заполнении для правильного выравнивания. Или это на самом деле мой компилятор, и мне нужно что-то вроде __attribute__((__packed__)), чтобы гарантировать это?

Ответы [ 3 ]

3 голосов
/ 20 мая 2019

Не должно быть проблем с заполнением, определяемым реализацией, однако в зависимости от порядка байтов будут различия между представлениями. Также обратите внимание, что выравнивание будет другим - это становится актуальным, например, если вы встраиваете свои значения в другую структуру.

В целом, неясно, какого уровня совместимости протоколов вы пытаетесь достичь. Я предлагаю вам либо принять решение о том, что вы разрешите нарушать совместимость протоколов от версии к версии, либо вы очень явно изложите свой протокол таким образом, чтобы он был расширяемым и версионным, чтобы могли взаимодействовать различные версии программного обеспечения. В этом случае вы должны разработать протокол так, чтобы он был четко определен независимо от вашей реализации C ++, и написать код отправки / получения в побайтном стиле, чтобы избежать проблем с порядком байтов.

Я не вижу, что пытается поддерживать одинаковый размер данных при изменении представления вообще.

1 голос
/ 23 мая 2019

Просто напишите правильные методы доступа:

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 на месте, поэтому вы можете memcpy или memmove класса столько, сколько хотите. Таким образом, получение через некоторый API, который использует указатели на char, unsigned char или std::byte, будет действительным.

Компилятор может вставлять отступы везде, кроме первого члена. Так что даже с одним полем он может вставить отступ в конце структуры - и, вероятно, мы могли бы найти странную реализацию, где sizeof(data_item_t) == sizeof(uint64_t). Правильный способ сделать это - написать правильные static_assertions.

0 голосов
/ 20 мая 2019

Это некрасиво, небезопасно и использует жестко закодированные магические числа.

Это хорошо известная идиома, и это одна из причин, по которой мы получили операторы битовых манипуляций, начиная с C. В этих числах нет "магии".

Другой вариант - позвонить по номеру std::memcpy, пока вы знаете свой порядок байтов. Это также легче обобщить, если это ваше дело.

Интересно, возможно ли сделать то же самое, определив двухэлементную структуру, которая "должна" быть точно такого же размера, что и uint32_t.

Не с 2-членной структурой, но вы можете сделать это, используя массив 2 uint16_t s - это гарантирует отсутствие заполнения между ними.

Вместо этого вы также можете использовать 2 члена по своему усмотрению, но утверждайте, что размер является минимальным. По крайней мере, таким образом вы гарантируете, что он будет работать, если он скомпилируется (что будет, на большинстве платформ в настоящее время):

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

Будет ли это работать эквивалентно битовой упаковке uint32_t с? Другими словами, будет ли массив элементов содержать те же биты, когда я использую struct data_item_t, как когда я использую uint32_t и битовую упаковку?

Нет, компилятор может добавить заполнение.

Или это на самом деле мой компилятор, и мне нужно что-то вроде __attribute__((__packed__)), чтобы гарантировать это?

Это смысл этого атрибута (в частности, для разных типов). : -)

...