Передача указателя на набор полей примитивных типов, расположенных в определённом порядке, - широко используемый паттерн. Так передаются указатели на структуры и объекты, массивы, файловые и сетевые буферы, данные в общей памяти и специальные типы (к примеру, массивы виртуальных функций), а отладчик, получив указатель на стек, может просматривать значения содержащихся в нём переменных. Если набор полей, доступный по указателю, содержит данные различных типов, обычной практикой является определение типов структур, описывающих расположение полей в наборе. Так, заголовочные файлы Windows содержат сотни подобных типов. Для C/C++, допускающих прямые арифметические операции над указателями, данная возможность является, скорее, синтаксическим сахаром, существенно упрощающим доступ к таким полям, тогда как для языков, строже следящих за типобезопасностью, она может быть единственной. Тем не менее, подход с определением структур имеет свои недостатки. Во-первых, количество типов имеет тенденцию быстро расти с усложнением API. Во-вторых, код работы с каждым типом структуры нужно писать (или генерировать) отдельно - или же делать типы шаблонными, теряя возможность дать каждому полю осмысленное имя. В-третьих, в ситуациях, когда порядок и типы полей не известны во время компиляции, написать соответствующий тип структуры попросту невозможно. В данной статье я хочу показать принятую в нашем проекте организацию работы с подобными наборами полей в стиле C++ - через соответствующие типы итераторов. Имеется указатель на последовательность двоичных полей примитивных типов, организованных в записи. Требуется определить итераторы, которые могут использоваться для перемещения по данной последовательности, чтения и записи отдельных полей. При этом: Внутри записи все поля выровнены по фиксированному размеру. Поля меньшего размера дополняются незначащими байтами, поля большего размера отсутствуют. Размер, по которому выравниваются поля, в одних случаях известен во время компиляции, в других (например, при возврате итератора из виртуальной функции, принимающей размер поля как аргумент) - во время выполнения. Поля последовательности могут быть одного типа или разных типов. В первом случае итератор работает как обычный итератор C++, во втором - допускает чтение и запись любых примитивных типов, размер которых не превышает текущий размер выравнивания полей. Порядок байтов в полях может совпадать с естественным порядком байтов данной платформы (big или little endian) или с сетевым порядком байтов (всегда big endian). В одних случаях порядок байтов известен во время компиляции, в других (например, при возврате итератора виртуальной функцией, осуществляющей доступ к локальным или доступным по сети данным) - только во время выполнения. Хранить и проверять состояние нужно во втором случае, но не в первом (оптимизация). На платформе, где сетевой порядок байтов совпадает с естественным, хранить и проверять состояние не нужно никогда. Поля, к которым осуществляется доступ, могут иметь гарантию правильного выравнивания или не иметь такой гарантии. В первом случае доступ к ним нужно проводить в конечных типах (соответствующие команды копирования многобайтных слов будут корректно работать на всех платформах), тогда как во втором случае требуется побайтовое копирование. Операции чтения и записи, перемещения по последовательности и произвольного доступа к её элементам должны осуществляться в обычной для итераторов C++ форме. Итератор может допускать доступ для чтения и записи или только для чтения (const-итератор). В качестве стандарта реализации был выбран C++20. В первой версии описываемого кода типов итератора было 4: binary_record_iterator, binary_record_const_iterator, specialized_binary_record_iterator и specialized_binary_record_const_iterator. Типы без суффикса const позволяли модифицировать последовательность, типы с таким суффиксом - не позволяли. Типы с префиксом specialized работали с последовательностями полей одного и того же типа, а типы без этого префикса - с последовательностями полей разных типов. Практика показала, что 90% кода этих типов совпадает или отличается незначительно, посему в конечном счёте четыре класса слились в один. Сам класс получился шаблонным, с четырьмя аргументами: Тип поля последовательности: void позволяет работать с любым типом, любой другой тип - только со значениями данного типа. Стратегия копирования содержимого полей. Стратегия определения размера поля. Булев флаг - признак константности последовательности, по которой осуществляется итерация. Стратегия копирования содержимого полей осуществляет непосредственно чтение и запись полей, доступных по указателю. Доступны несколько типов стратегий: Стратегия Описание endianness_converter Копирует поля по одному байту. Хранит флаг состояния, указывающий, сохраняется ли при доступе к полям порядок байтов или меняется на обратный. endianness_keeper Копирует поля по одному байту. Всегда сохраняет порядок байтов. Не имеет состояния. endianness_reverser Копирует поля по одному байту. Всегда меняет порядок байтов на обратный. Не имеет состояния. native_field_copier Копирует поля прямым присваиванием экземпляров конечного типа. Всегда сохраняет порядок байтов. Не имеет состояния. Необходимость побайтового копирования полей в ряде случаев может быть неочевидна. Тем не менее, существуют ситуации, когда просто так привести указатель к типу значения, разыменовать и выполнить присваивание нельзя - например, если данные прочитаны из файла или приняты по сети и находятся в буфере с неверным выравниванием. В зависимости от компилятора и процессора операция прямого присваивания таких значений может дать неверный результат, поскольку определённые команды копирования многобайтовых слов предполагают, что данные размещены в памяти с корректным выравниванием. В своё время ваш покорный слуга наступил на эти грабли при разработке SCADA-системы на базе ARM-компьютера Moxa UC-7110. При чтении значения с плавающей точкой, доступного по указателю, оно отличалось от ожидаемого, хотя побайтовое сравнение показывало идентичность эталону. Использование функции memcpy() вместо простого разыменования с присваиванием позволило решить проблему. В то же время, в случаях, когда значения гарантированно расположены в памяти с корректным выравниванием, побайтового копирования хотелось бы избежать. Стратегия native_field_copier позволяет перейти к многобайтным инструкциям, что обычно быстрее. Таким образом, при чтении значений, гарантированно имеющих естественный порядок байтов, используются native_field_copier или endianness_keeper, в зависимости от наличия гарантий на выравнивание полей. В случаях, когда порядок полей нужно всегда менять на противоположный, используется endianness_reverser. Наконец, если алгоритм должен работать как с прямым, так и с обратным порядком байтов, используется стратегия endianness_converter - самая тяжёлая за счёт хранения и проверки флага состояния. Для проверки того, какое выравнивание является естественным для данной платформы, используется заголовочник <bit>, который как раз подвезли в C++20. Фактически используемые типы стратегий копирования значений определены следующим образом: local_or_network_field_copier используется там, где требуется доступ к данным, полученным локально или по сети, а network_field_copier - там, где ведётся работа исключительно с сетевыми данными. Такое определение переводит максимальное количество проверок на этап компиляции. Там, где данные заведомо имеют корректное выравнивание, используется native_field_copier. Стратегий определения размера поля всего две, и они гораздо проще: runtime_record_size Позволяет менять размер поля во время выполнения. Содержит состояние - размер поля. fixed_record_size Размер поля определяется только во время компиляции (аргументом шаблона). Не имеет состояния. Итераторы предоставляют стандартные операции: пре- и пост-инкремент и декремент, а также операторы +, -, += и -= с аргументами типа size_t и ptrdiff_t. Единицей перемещения является поле, размер которого определяется текущей стратегией ширины поля. Для определения взаимного расположения двух итераторов доступен полный комплект операторов сравнения, а также оператор -. Присутствует также синтаксический сахар: операторы проверки на (не)равенство nullptr и дублирующие их операторы ! и приведения к bool. Для доступа к данным используются операторы разыменования и доступа по индексу. Кроме того, доступны методы для чтения (у любых итераторов) и записи (у неконстантных) полей с автоматическим перемещением к следующему полю, а также для получения текущих размера поля и указателя на данные. Также есть специальный метод для чтения 64-битной записи в тип size_t с контролем переполнения для 32- и менее битных платформ. Метод specialize() используется для перехода от нетипизированных итераторов к типизированным. Метод with_record_size фиксирует размер поля, заменяя текущую стратегию стратегией fixed_record_size с переданным аргументом шаблона. Экземпляры стратегий копирования байтов и определения размера поля включены в экземпляр итератора - закрытым наследованием, чтобы сэкономить место при отсутствии состояния у стратегии. Итератор, сконструированный конструктором по умолчанию или с единственным nullptr-аргументом, не указывает на какие-либо данные. Также доступны конструкторы копирования и перемещения и соответствующие операторы присваивания. Основные же конструкторы принимают обязательный указатель на данные (const void* для константного итератора, void* для обычного) и аргументы стратегий. В качестве аргументов стратегий могут быть переданы экземпляры самих стратегий. Альтернативно, для стратегии копирования данных можно передать порядок байт (std::endian) или признак его инверсии, а для стратегии определения размера поля - значение этого самого размера в байтах. Стратегии, допускающие изменение параметра в рантайме, сохраняют переданное значение, а не имеющие состояния - проверяют (через assert), что переданное значение соответствует их поведению. Аргументы для стратегий, имеющих состояние, обязательны, для не имеющих состояния - опциональны. Указатель на данные всегда хранится как char* или const char* для удобства навигации по полям. Операции перемещения по данным выглядят тривиально: Здесь is_forward_offset_valid() - простейшая проверка переполнения при выполнении арифметических операций с указателями. Поскольку данных о доступном диапазоне данных у итератора нет, выход за его границы не контролируется. Для совместимости со стандартными алгоритмами определены типы-члены iterator_category, value_type, difference_type, reference и pointer. Доступ к значению при разыменовании итератора осуществляется при помощи вспомогательного класса binary_record_reference, имеющего те же аргументы шаблона, что и класс итератора. Как и итератор, binary_record_reference содержит указатель на данные типа char* или const char*, а также наследует стратегии копирования данных и определения размера поля в закрытом режиме. Первая стратегия используется по прямому назначению, вторая - для проверки того, что размер копируемого значения не превышает размера поля. Существует две специализации binary_record_reference. Первая работает с итераторами полей фиксированного типа и допускает чтение и запись только значений данного типа. Вторая работает с последовательностями полей разных типов и позволяет читать и писать значения любых типов, размер которых не превышает размера поля, заданного стратегией. Чтение и запись поля фиксированного типа выглядят тривиально: Чтение и запись полей произвольных типов реализованы аналогично: Поскольку возможность получить корректный указатель на значение хранимого типа в общем случае не гарантируется, binary_record_reference не переопределяет оператор взятия адреса. Система, ради которой писалась данная обёртка, оперирует пакетами двоичных данных, состоящими из двух частей: данных фиксированного размера и данных переменного размера. Часть фиксированного размера организована как последовательность 64-битных полей. Как правило, одно поле содержит одно примитивное значение размером до 64 бит (незанятые байты игнорируются). Единственное исключение - массивы фиксированного размера, элементы которых располагаются в стык и могут занимать более одного поля: так, массив из пяти 16-битных значений займёт два 64-битных поля, внутри которых первые 80 бит содержат значения, а оставшиеся 48 бит не используются. Данные динамического размера используются для передачи контейнеров произвольной длины и содержат элементы этих самых контейнеров, упакованные без дырок. Ссылки на такие контейнеры размещаются в части фиксированного размера в виде двух 64-битных полей: количества элементов и адреса. Итераторы, служащие для чтения данных, определены так: Алгоритмы чтения данных работают с базовым абстрактным типом пакета: Аргументы field_count и element_count служат для проверки выхода за границы доступных данных. Существует два типа пакетов. Первый описывает данные, размещённые в оперативной памяти: имеющие естественный порядок байтов и использующие значение указателя в качестве адреса динамических данных (dynamic_data_pointer). Второй описывает пакет, полученный по сети: данные имеют сетевой порядок байтов, а в качестве адреса динамических данных используется смещение от начала пакета. Благодаря слоям абстракции, реализованным классами пакета и итератора, алгоритму чтения данных не нужно ничего знать об их происхождении. Следующая врезка демонстрирует чтение нескольких значений из пакета в структуру. Запись будет выглядеть аналогично. Пример выше показывает работу с итераторами, полученными от пакета, однако не раскрывает весь потенциал данного решения. Типы всех читаемых из пакета полей известны заранее, и того же результата можно было бы добиться приведением указателя на данные пакеты к типу структуры из четырёх полей (с последующей инверсией порядка байтов при необходимости). Удобство от работы с такими "итераторами по void*" становится более очевидным при написании обобщённого кода, не привязанного к конкретным типам. Например, следующий пример извлекает из пакета последовательность полей произвольных типов и передаёт их некому функтору: Теперь шаблон argument_sequence_handler можно использовать для передачи в функтор произвольного числа аргументов произвольных типов, извлечённых из пакета: Поддержка новых типов добавляется элементарно - через специализацию type_traits. Аналогичным образом можно добавить алгоритмы для сохранения данных в пакет. Таким образом, удалось добиться разделения обязанностей между несколькими классами: Класс binary_record_iterator отвечает за перемещение по байтовым последовательностям и за предоставление высокоуровневого интерфейса доступа к ним. Стратегии копирования значений отвечают за доступ к данным и конверсию порядка байтов при необходимости. Стратегии размера поля следят за упаковкой данных с дырками или без дырок. Классы, производные от data_pack, отвечают за хранение данных и получение итераторов на них, а также сообщают итератору, какой порядок байтов используется в конкретном пакете. Класс type_traits отвечает за чтение и запись конкретных типов, получая всю необходимую информацию о расположении данных от пакета и argument_sequence_handler. Наконец, argument_sequence_handler берёт на себя работу по упорядочению работы с отдельными значениями, закодированными в последовательности. В нашем проекте данный подход используется при взаимодействии модулей с ядром, а также при передаче пакетов данных по сети. Это позволяет расширять API взаимодействия (добавлять новые последовательности полей) без раздувания кода - простой специализацией соответствующих шаблонов. Помимо примитивных типов и строк, система позволяет обмениваться массивами фиксированного и динамического размера, а также дескрипторами объектов. Также планируется добавить поддержку структур. Основные классы итератора Стратегии копирования полей и размера записи Пакеты данных: раз, два, три Обработчик конкретных типов стороны ядра и стороны модуля argument_sequence_handler стороны ядра и стороны модуля Пример теста, много оперирующего описанными итераторами  Разработчик