Выравнивание элементов данных С++ и массива
Во время обзора кода я столкнулся с некоторым кодом, который определяет простую структуру следующим образом:
class foo {
unsigned char a;
unsigned char b;
unsigned char c;
}
В другом месте определяется массив этих объектов:
foo listOfFoos[SOME_NUM];
Позже структуры скопированы в буфер:
memcpy(pBuff,listOfFoos,3*SOME_NUM);
Этот код основан на предположениях, что: a.) Размер foo равен 3, и никакое дополнение не применяется, и b.) Массив из этих объектов упакован без прокладки между ними.
Я пробовал это с GNU на двух платформах (RedHat 64b, Solaris 9), и он работал на обоих.
Правильны ли предположения? Если нет, при каких условиях (например, изменение в OS/компиляторе) они могут не работать?
Ответы
Ответ 1
Массив объектов должен быть смежным, поэтому между объектами никогда не набивается, хотя дополнение может быть добавлено к концу объекта (создавая почти тот же эффект).
Учитывая, что вы работаете с char, предположения, вероятно, правильнее, чем обычно, но стандарт С++, конечно же, не гарантирует этого. Другой компилятор или даже просто изменение флагов, переданных вашему текущему компилятору, может привести к добавлению прокладки между элементами структуры или после последнего элемента структуры или и того и другого.
Ответ 2
Было бы безопаснее делать:
sizeof(foo) * SOME_NUM
Ответ 3
Если вы скопируете массив таким образом, вы должны использовать
memcpy(pBuff,listOfFoos,sizeof(listOfFoos));
Это всегда будет работать, пока вы выделили pBuff одинакового размера.
Таким образом, вы не делаете никаких предположений о заполнении и выравнивании вообще.
Большинство компиляторов объединяют структуру или класс с требуемым выравниванием самого большого включенного типа. В вашем случае символов, которые не означают выравнивание и отступы, но если вы добавите короткий, например, ваш класс будет иметь 6 байтов с одним байтом заполнения, добавленным между последним char и вашим коротким.
Ответ 4
Я думаю, причина в том, что это работает, потому что все поля в структуре char, которые выравнивают один. Если есть хотя бы одно поле, которое не выравнивает 1, выравнивание структуры/класса будет не равно 1 (выравнивание будет зависеть от порядка поля и выравнивания).
Посмотрим на пример:
#include <stdio.h>
#include <stddef.h>
typedef struct {
unsigned char a;
unsigned char b;
unsigned char c;
} Foo;
typedef struct {
unsigned short i;
unsigned char a;
unsigned char b;
unsigned char c;
} Bar;
typedef struct { Foo F[5]; } F_B;
typedef struct { Bar B[5]; } B_F;
#define ALIGNMENT_OF(t) offsetof( struct { char x; t test; }, test )
int main(void) {
printf("Foo:: Size: %d; Alignment: %d\n", sizeof(Foo), ALIGNMENT_OF(Foo));
printf("Bar:: Size: %d; Alignment: %d\n", sizeof(Bar), ALIGNMENT_OF(Bar));
printf("F_B:: Size: %d; Alignment: %d\n", sizeof(F_B), ALIGNMENT_OF(F_B));
printf("B_F:: Size: %d; Alignment: %d\n", sizeof(B_F), ALIGNMENT_OF(B_F));
}
При выполнении результат:
Foo:: Size: 3; Alignment: 1
Bar:: Size: 6; Alignment: 2
F_B:: Size: 15; Alignment: 1
B_F:: Size: 30; Alignment: 2
Вы можете видеть, что Bar и F_B имеют выравнивание 2, так что его поле я будет правильно выровнено. Вы также можете видеть, что размер бара 6, а не 5. Аналогично, размер B_F (5 баров) равен 30, а не 25.
Итак, если вы являетесь жестким кодом вместо sizeof(...)
, здесь вы получите проблему.
Надеюсь, что это поможет.
Ответ 5
Я был бы в безопасности и заменил магическое число 3 на sizeof(foo)
Я считаю.
Я предполагаю, что код, оптимизированный для будущих архитектур процессоров, вероятно, представит некоторую форму дополнения.
И попытка найти такую ошибку - настоящая боль!
Ответ 6
Все сводится к выравниванию памяти. Типичные 32-битные машины считывают или записывают 4 байта памяти за попытку. Эта структура защищена от проблем, поскольку она легко подпадает под эти 4 байта без каких-либо проблем с путаницей.
Теперь, если структура была такой:
class foo {
unsigned char a;
unsigned char b;
unsigned char c;
unsigned int i;
unsigned int j;
}
Логика ваших коллег, вероятно, приведет к
memcpy(pBuff,listOfFoos,11*SOME_NUM);
(3 char= 3 байта, 2 ints = 2 * 4 байта, поэтому 3 + 8)
К сожалению, из-за заполнения структура фактически занимает 12 байтов. Это связано с тем, что вы не можете вместить три char и int в это 4-байтовое слово, и поэтому там есть один байт заполненного пространства, который толкает int в его собственное слово. Это становится все более и более проблемой, чем более разнообразны типы данных.
Ответ 7
В ситуациях, когда такие вещи используются, и я не могу этого избежать, я пытаюсь сделать перерыв компиляции, когда презумпции больше не сохраняются. Я использую что-то вроде следующего (или Boost.StaticAssert, если это позволяет ситуация):
static_assert(sizeof(foo) <= 3);
// Macro for "static-assert" (only usefull on compile-time constant expressions)
#define static_assert(exp) static_assert_II(exp, __LINE__)
// Macro used by static_assert macro (don't use directly)
#define static_assert_II(exp, line) static_assert_III(exp, line)
// Macro used by static_assert macro (don't use directly)
#define static_assert_III(exp, line) enum static_assertion##line{static_assert_line_##line = 1/(exp)}
Ответ 8
Как говорили другие, использование sizeof (foo) - более безопасная ставка. Некоторые компиляторы (особенно эзотерические во встроенном мире) добавят 4-байтовый заголовок к классам. Другие могут выполнять фанковые трюки для выравнивания памяти, в зависимости от ваших настроек компилятора.
Для основной платформы вы, вероятно, в порядке, но это не гарантия.
Ответ 9
По-прежнему может возникнуть проблема с sizeof() при передаче данных между двумя компьютерами. На одном из них код может компилироваться с дополнением и в другом без, и в этом случае sizeof() даст разные результаты. Если данные массива передаются с одного компьютера на другой, он будет неверно истолкован, потому что элементы массива не будут найдены там, где это ожидалось.
Одно из решений - убедиться, что #pragma pack (1) используется по возможности, но этого может быть недостаточно для массивов. Лучше всего предвидеть проблему и использовать отступы до нескольких байтов на элемент массива.