Struct padding в C++
Если у меня есть struct
в C++ нет способа безопасно прочитать / записать его в файл, который является кросс-платформенным / совместимым с компилятором?
Потому что, если я правильно понимаю, каждый компилятор по-разному "зависит" от целевой платформы.
5 ответов
Нет, это невозможно. Это из-за отсутствия стандартизации C++ на двоичном уровне.
Дон Бокс пишет (цитата из его книги Essential COM, глава COM As A Better C++)
C++ и Переносимость
После того как принято решение распространять класс C++ в виде DLL, возникает один из фундаментальных недостатков C++, а именно отсутствие стандартизации на двоичном уровне. Хотя рабочий документ ISO/ANSI C++ пытается систематизировать, какие программы будут компилироваться и каковы будут семантические эффекты их запуска, он не пытается стандартизировать бинарную модель времени исполнения C++. Впервые эта проблема станет очевидной, когда клиент попытается соединиться с библиотекой импорта DLL-библиотеки FastString из среды разработки C++, отличной от той, которая использовалась для создания DLL-библиотеки FastString.
Структурное заполнение выполняется разными компиляторами по-разному. Даже если вы используете один и тот же компилятор, выравнивание упаковки для структур может отличаться в зависимости от того, какой пакет pragma вы используете.
Мало того, что если вы напишите две структуры, члены которых в точности совпадают, единственное различие заключается в том, что порядок, в котором они объявлены, различен, тогда размер каждой структуры может быть (и часто) разным.
Например, посмотрите это,
struct A
{
char c;
char d;
int i;
};
struct B
{
char c;
int i;
char d;
};
int main() {
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
}
Скомпилируйте это с gcc-4.3.4
и вы получите этот вывод:
8
12
То есть размеры разные, хотя обе структуры имеют одинаковых членов!
Код в Ideone: http://ideone.com/HGGVl
Суть в том, что стандарт не говорит о том, как должно выполняться заполнение, и поэтому компиляторы могут принимать любые решения, и вы не можете предполагать, что все компиляторы принимают одно и то же решение.
Если у вас есть возможность спроектировать структуру самостоятельно, это должно быть возможно. Основная идея заключается в том, что вы должны создать его так, чтобы не было необходимости вставлять в него байты пэда. Вторая хитрость заключается в том, что вы должны обрабатывать различия в порядке байтов.
Я опишу, как построить структуру, используя скаляры, но вы должны иметь возможность использовать вложенные структуры, если вы примените один и тот же дизайн для каждой включенной структуры.
Во-первых, основным фактом в C и C++ является то, что выравнивание типа не может превышать размер типа. Если бы это было так, то было бы невозможно выделить память, используя malloc(N*sizeof(the_type))
,
Разметить структуру, начиная с самых больших типов.
struct
{
uint64_t alpha;
uint32_t beta;
uint32_t gamma;
uint8_t delta;
Затем добавьте структуру вручную, чтобы в итоге вы выбрали самый большой тип:
uint8_t pad8[3]; // Match uint32_t
uint32_t pad32; // Even number of uint32_t
}
Следующим шагом является определение того, должна ли структура храниться в формате с прямым или младшим порядком байтов. Лучший способ - это "поменять" все элементы на месте перед записью или после чтения структуры, если формат хранения не соответствует порядку байтов системы хоста.
Нет, безопасного пути нет. В дополнение к заполнению вы должны иметь дело с различным порядком байтов и различными размерами встроенных типов.
Вам необходимо определить формат файла и преобразовать свою структуру в этот формат и из него. Библиотеки сериализации (например, boost::serialization или буфер протокола Google) могут помочь в этом.
Короче говоря, нет. Не существует независимого от платформы, стандартно-совместимого способа работы с отступами.
В Стандарте заполнение называется "выравниванием", и оно начинает обсуждаться в 3.9/5:
Типы объектов имеют требования к выравниванию (3.9.1, 3.9.2). Выравнивание полного типа объекта представляет собой целочисленное значение, определяемое реализацией, представляющее количество байтов; объект размещается по адресу, который соответствует требованиям выравнивания его типа объекта.
Но это продолжается и отходит ко многим темным углам Стандарта. Выравнивание является "определяемым реализацией", что означает, что оно может быть различным для разных компиляторов или даже для моделей адресов (то есть 32-битных /64-битных) в рамках одного и того же компилятора.
Если у вас нет действительно жестких требований к производительности, вы можете рассмотреть вопрос о сохранении ваших данных на диске в другом формате, например, в виде строк символов. Многие высокопроизводительные протоколы отправляют все, используя строки, когда естественный формат может быть чем-то другим. Например, канал обмена с низкой задержкой, над которым я недавно работал, отправляет даты в виде строк, отформатированных так: "20110321", а время отправляется аналогично: "141055.200". Несмотря на то, что этот канал обмена сообщениями отправляет 5 миллионов сообщений в секунду в течение всего дня, они все равно используют строки для всего, потому что таким образом они могут избежать ошибок и других проблем.