Существует ли переносная альтернатива битовым полям С++
Существует много ситуаций (особенно при программировании на низком уровне), где важна двоичная компоновка данных. Например: манипуляция аппаратными средствами/драйверами, сетевые протоколы и т.д.
В С++ я могу читать/писать произвольные двоичные структуры, используя char*
и побитовые операции (маски и сдвиги), но это утомительное и подверженное ошибкам. Очевидно, я пытаюсь ограничить объем этих операций и инкапсулировать их в API более высокого уровня, но это все равно боль.
бит-биты С++, похоже, предлагают решение для этой проблемы, но, к сожалению, их хранилище .
NathanOliver упоминает std::bitset
, который в основном позволяет вам обращаться к отдельным битам целого числа с хорошим operator[]
, но не имеет аксессуаров для многобитовых полей.
Используя метапрограммирование и/или макросы, можно абстрагировать побитовые операции в библиотеке. Так как я не хочу изобретать колесо, я ищу библиотеку (желательно STL или boost), которая делает это.
Для записи я рассматриваю это для DNS resolver, но проблема и ее решение должны быть общими.
Изменить: короткий ответ: получается, что хранение битового поля является надежным на практике (даже если оно не предусмотрено стандартом), поскольку системные/сетевые библиотеки используют их, а yeild - хорошо управляемые программы при компиляции с помощью основного потока составители.
Ответы
Ответ 1
У нас есть это в производственном коде, где нам нужно было переносить код MIPS на x86-64
https://codereview.stackexchange.com/questions/54342/template-for-endianness-free-code-data-always-packed-as-big-endian
Хорошо работает для нас.
В основном это шаблон без какого-либо хранилища, аргументы шаблона определяют положение соответствующих битов.
Если вам нужно несколько полей, вы помещаете несколько специализированных объединений в объединение вместе с массивом байтов для хранения.
У шаблона есть перегрузки для присваивания значения и оператора преобразования для 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;
}
Ответ 2
Из стандарта С++ 14 (проект N3797), раздел 9.6 [class.bit], пункт 1:
Распределение бит-полей внутри объекта класса определяется реализацией. Выравнивание битовых полей определяется реализацией. Бит-поля упаковываются в некоторую адресную единицу распределения. [Примечание: бит-поля выделяют единицы размещения на некоторых машинах, а не другие. Бит-поля назначаются справа налево на некоторых машинах, слева направо на других. - конечная нота]
Несмотря на то, что примечания являются ненормативными, каждая реализация, о которой я знаю, использует один из двух макетов: либо большой, либо немного порядковый бит.
Обратите внимание, что:
- Вы должны указать заполнение вручную. Это означает, что вы должны знать размер ваших типов (например, с помощью
<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
// ...
}
И поскольку он работает с компиляторами основного потока, это означает, что наборы памяти для битов памяти на практике надежны.
Edit:
Это переносимо в пределах одной цели:
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;
};
Ответ 3
Проще реализовать битовые поля с известными позициями с помощью С++:
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;
}
};
Вышеупомянутая реализация игрушек позволяет, например, определить 12-битное поле в переменной unsigned long long
с
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
В основном, что вам нужно было бы написать вручную
Ответ 4
Я написал реализацию битовых полей в С++ в качестве файла заголовка библиотеки. Пример, который я приводил в документации, заключается в том, что вместо того, чтобы писать это:
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;
Используя эту реализацию битовых полей, параметр шаблона признаков дает программисту большую гибкость. По умолчанию память - это только память процессора, или она может быть абстракцией, а программист предоставляет функции для чтения и записи "памяти". Абстрагируемая память представляет собой последовательность элементов любого неподписанного интегрального типа (выбираемого программистом). Поля могут быть отложены либо от наименьшего, либо от наименьшего значения. Макет полей в памяти может быть обратным тому, что они находятся в структуре формата.
Реализация находится по адресу: https://github.com/wkaras/C-plus-plus-library-bit-fields
(Как вы можете видеть, я, к сожалению, не смог полностью избежать использования макросов.)
Ответ 5
C предназначен для малоуровневой обработки бит. Это достаточно просто, чтобы объявить буфер без знака символов и установить его на любой бит, который вы хотите. Особенно, если ваши битовые строки очень короткие, поэтому вписываются в один из интегральных типов.
Одна потенциальная проблема - это байт-сущность. C не может "видеть" это вообще, но так же, как целые числа имеют сущность, так же как и байты, когда сериализуются. Другим является очень небольшое число машин, которые не используют октеты для байтов. C гарантирует, что байт должен быть, по крайней мере, октетом, но 32 и 9 являются реалиями реального мира. В этих обстоятельствах вам необходимо принять решение о том, следует ли просто игнорировать старшие биты (в этом случае наивный код должен работать) или рассматривать их как часть потока битов (в этом случае вы должны быть осторожны, чтобы сбросить CHAR_BIT в ваши расчеты). Также сложно проверить код, так как вам вряд ли удастся получить доступ к машине CHAR + BIT 32.