Читання і запис бітів у потік — типова задача. У цій статті оглянуто методи, з якими стикався автор, і пропонується один в стилі метапрограмування С++. Серед традиційних найбільш помітні такі:

  • Застосувати побітові операції — рутинна робота, проте згенерований код виходить ефективний
  • Використати бітові поля — ефективний код, менше рутини, але початковий код може бути заплутаний і не дуже портовний між платформами з різним порядком байтів
  • Створити власний клас, який би відслідковував читання окремих частин байтів під час виконання — у вебі є багато прикладів.

Ось інший підхід, який настільки ж ефективний, як і ручний код, але набагато зручніший. Хай компілятор наперед знає якомога більше, і матиме змогу генерувати спеціалізований оптимізований машинний код. Розгляньмо файл з початковим кодом BitStream.cpp. В ньому визначені дві шаблонні функції:

// Прочитати 'count' бітів з пам’яті за адресою біту 'offset'.
// Компілятор легко зоптимізує згенерований код.
template <unsigned offset, unsigned count>
inline uint32_t Read(const uint8_t *src, uint32_t accum = 0);

// Записати задане value у пам’ять за адресою біту 'offset'
// шириною 'count' бітів
template <unsigned offset, unsigned count>
inline void Write(uint8_t *dst, uint32_t value);

Для кожної комбінації параметрів offset і count, генерується цільова функція, довжиною всього кілька інструкцій. Використовувати їх легко. Наприклад, розгляньмо розбір заголовка RTP:

const uint8_t *packet = ...;
uint32_t V = Read<0,2>(packet);
uint32_t P = Read<2,1>(packet);
uint32_t X = Read<3,1>(packet);
uint32_t CC = Read<4,4>(packet);
uint16_t seqNum = Read<16,16>(packet);

В якості ілюстрації, можна подивитися згенерований код у compiler explorer (посилання):

Читання з бітового потоку у C++

Недоліком такого підходу, на мою думку, є те, що згенерований код залежить від налаштунків оптимізації компілятора. Таким чином, налагоджувальна збірка може працювати помітно повільніше.