Есть ли переносимая альтернатива битовым полям С++?

Во многих ситуациях (особенно в низкоуровневом программировании) важна двоичная структура данных. Например: манипулирование оборудованием/драйвером, сетевыми протоколами и т. д.

В C++ я могу читать/записывать произвольные двоичные структуры, используя char* и побитовые операции (маски и сдвиги), но это утомительно и подвержено ошибкам. Очевидно, я пытаюсь ограничить объем этих операций и инкапсулировать их в API более высокого уровня, но это все еще проблема.

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

Натан Оливер упомянул std::bitset, который в основном позволяет вам получить доступ к отдельным битам целого числа с помощью красивого operator[], но не имеет средств доступа для многобитных полей.

Используя метапрограммирование и/или макросы, можно абстрагировать побитовые операции в библиотеке. Поскольку я не хочу изобретать велосипед, я ищу библиотеку (желательно STL или boost), которая это делает.

Для протокола: я ищу здесь DNS, но проблема и ее решение должны быть общими.

Изменить: короткий ответ: оказывается, хранение битовых полей на практике надежно (даже если это не предусмотрено стандартом), поскольку системные/сетевые библиотеки используют их и дают программы с хорошим поведением при компиляции с помощью основных компиляторов.


person Antoine    schedule 30.07.2015    source источник
comment
Я думаю, вам нужно написать свои собственные функции, чтобы справиться с этим (и, или,...)   -  person    schedule 30.07.2015
comment
Иногда я подхожу к этому, абстрагируя манипуляции с битами во время компиляции, используя метапрограммирование.   -  person shuttle87    schedule 30.07.2015
comment
@Kilanny: да, я обычно так и делаю, но просто интересно, есть ли способ сделать такой код более удобным в сопровождении   -  person Antoine    schedule 30.07.2015
comment
@shuttle87: интересно, сейчас я обдумываю эту идею, но стараюсь не изобретать велосипед ;)   -  person Antoine    schedule 30.07.2015
comment
Вы смотрели на std::bitset   -  person NathanOliver    schedule 30.07.2015
comment
@NathanOliver: +1 кажется лучше, чем битовые поля. Удобный доступ к отдельным битам, но не хватает средств доступа для полей более одного бита (например, 4-битное целое, расположенное в битах 3..7)   -  person Antoine    schedule 30.07.2015
comment
Собирался ответить на этот вопрос, но он был закрыт, может быть, мне следует написать запись в блоге о том, как я на самом деле сделал это во встроенном проекте, над которым работал.   -  person shuttle87    schedule 30.07.2015
comment
@ Антуан Я не думаю, что было бы так сложно написать функцию, возвращающую диапазон битов. Я знаю, что сейчас это не приносит вам никакой пользы, но вы могли бы предложить это комитету по стандартизации.   -  person NathanOliver    schedule 30.07.2015
comment
@NathanOliver: спасибо, я напишу свой собственный. Я просто просил лучшие практики, но ладно ~   -  person Antoine    schedule 30.07.2015
comment
Не по теме ? С каких это пор, как мне это сделать, вопросы не по теме?   -  person Quentin    schedule 30.07.2015
comment
@Quentin Я воспринял So this left me wondering if there was a nice C++ language-feature or library (preferably STL or boost) for reading/writing bitfields ? как просьбу recommend or find a book, tool, software library, tutorial or other off-site resource   -  person NathanOliver    schedule 30.07.2015
comment
@NathanOliver (и другие) (ИМХО) Вопросы, на которые дается эта причина VTC, обычно представляют собой очень общие вопросы (Где я могу узнать X?), На которые нельзя ответить самостоятельным образом. Этот вопрос очень сфокусирован, и на него можно ответить с помощью специального кода, стандартной функциональности, Boost или другой библиотеки. Учитывая относительно небольшую проблему, я бы сказал, что автономные специальные ответы вероятны, и это хорошо. На самом деле у меня есть один такой ответ.   -  person Quentin    schedule 30.07.2015
comment
Это не должно быть сложно реализовать с помощью С++... например, что-то вроде BitField<unsigned long, 3, 12>, представляющего собой битовое поле, выделенное в битах 3...14 unsigned long (где конструктор должен предоставить адрес беззнакового длинного) и поддерживающее назначение из целых чисел и неявное преобразование в целое число.   -  person 6502    schedule 30.07.2015
comment
@ shuttle87 - вопрос был открыт повторно - возможно, вы также могли бы указать свой ответ.   -  person Soren    schedule 30.07.2015


Ответы (5)


У нас есть это в производственном коде, где нам пришлось портировать код 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;
}
person CplusPuzzle    schedule 16.08.2017

Из стандарта С++ 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
    // ...
}

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

Редактировать:

Это переносимо в пределах одного порядка байтов:

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;
};
person o11c    schedule 30.07.2015
comment
Вы говорите, что все основные компиляторы плотно упаковывают битовые поля (и поля могут охватывать байты), а уровень оптимизации не влияет на двоичную структуру битовых полей? Вы проверили себя или можете дать ссылку на какую-то информацию, указывающую вам путь? - person Antoine; 30.07.2015
comment
Да, при условии, что вы не выходите за границы единицы и не расширяетесь до этого размера. Если бы оптимизация повлияла на структуру структуры, вся вселенная сгорела бы. - person o11c; 30.07.2015
comment
Действительно, заголовок tcp.h убедил меня в том, что на самом деле битовые поля можно использовать, даже если здесь, на SO, о них много чрезмерно осторожных сообщений. - person Antoine; 30.07.2015
comment
Я считаю, что стандарт говорит о порядке битовых полей, а не о порядке битов. - person HCSF; 28.03.2019
comment
@HCSF Все реализации выбирают дополнительные ограничения помимо требований стандарта. Это одна из вещей, на которые вы можете положиться. - person o11c; 28.03.2019

С помощью 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;
    }
};

Приведенная выше игрушечная реализация позволяет, например, определить 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   

В основном то, что вам придется писать от руки

person 6502    schedule 30.07.2015
comment
Вы можете избежать накладных расходов на указатель/память, поместив их в союз. Кроме того, вы должны использовать size_t и T вместо int местами и `static_assert(std::is_unsigned‹T›::value, битовые поля должны быть беззнаковыми, чтобы быть нормальными). - person o11c; 30.07.2015

Я написал реализацию битовых полей на 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;

Используя эту реализацию битовых полей, параметр шаблона признаков дает программисту большую гибкость. Память — это просто память процессора по умолчанию, или она может быть абстракцией, когда программист предоставляет функции для выполнения чтения и записи «памяти». Абстрагированная память представляет собой последовательность элементов любого беззнакового целочисленного типа (выбирается программистом). Поля могут располагаться либо от наименьшего к наибольшему, либо от наибольшего к наименьшему значению. Расположение полей в памяти может быть обратным их расположению в структуре формата.

Реализация находится по адресу: https://github.com/wkaras/C-plus-plus-library-bit-fields

(Как видите, мне, к сожалению, не удалось полностью избежать использования макросов.)

person WaltK    schedule 11.03.2017

C предназначен для низкоуровневых манипуляций с битами. Достаточно просто объявить буфер беззнаковых символов и установить для него любой битовый шаблон, который вы хотите. Особенно, если ваши битовые строки очень короткие, поэтому вписываются в один из целочисленных типов.

Одной из потенциальных проблем является порядок следования байтов. C вообще не может «видеть» это, но так же, как целые числа имеют порядок следования байтов, так же и байты при сериализации. Другой причиной является очень небольшое количество машин, которые не используют октеты для байтов. C гарантирует, что байт должен быть не менее октета, но 32 и 9 являются реальными реализациями. В этих обстоятельствах вы должны принять решение, игнорировать ли старшие биты (в этом случае наивный код должен работать) или рассматривать их как часть битового потока (в этом случае вы должны быть осторожны, складывая CHAR_BIT в ваши расчеты). Также сложно протестировать код, так как вам вряд ли будет легко заполучить машину CHAR+BIT 32.

person Malcolm McLean    schedule 11.03.2017