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

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

В С++ я могу читать/писать произвольные двоичные структуры, используя 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.