Выравнивание элементов данных C++ и упаковка массива
во время обзора кода я столкнулся с некоторым кодом, который определяет простую структуру следующим образом:
class foo {
unsigned char a;
unsigned char b;
unsigned char c;
}
в другом месте определяется массив этих объектов:
foo listOfFoos[SOME_NUM];
позже, структуры raw-копируются в буфер:
memcpy(pBuff,listOfFoos,3*SOME_NUM);
этот код основан на предположениях, что: a.) Размер foo 3, и никакая прокладка не приложена, и b.) Массив этих объектов упакован без заполнения между ними.
Я пробовал это с GNU на две платформы (RedHat 64b, Solaris 9), и она работала на обеих.
предположения выше допустимого? Если нет, то при каких условиях (например, изменение в ОС/компиляторе) они могут потерпеть неудачу?
9 ответов
массив объектов должен быть непрерывным, поэтому между объектами никогда нет заполнения, хотя заполнение может быть добавлено в конец объекта (производя почти тот же эффект).
учитывая, что вы работаете с char, предположения, вероятно, правильны чаще, чем нет, но стандарт C++, безусловно, не гарантирует этого. Другой компилятор или даже просто изменение флагов, переданных вашему текущему компилятору, может привести к вставке заполнения между элементы структуры или следующие за последним элементом структуры,или оба.
Если вы копируете свой массив таким образом, вы должны использовать
memcpy(pBuff,listOfFoos,sizeof(listOfFoos));
это всегда будет работать, пока вы выделили pBuff того же размера. Таким образом, вы не делаете никаких предположений о заполнении и выравнивании вообще.
большинство компиляторов выравнивают структуру или класс по требуемому выравниванию самого большого включенного типа. В вашем случае символов это означает отсутствие выравнивания и заполнения, но если вы добавите короткий, например, ваш класс будет 6 байтов с одним байтом заполнения между последней и короткой.
Я думаю, что причина в том, что это работает, потому что все поля в структуре являются символами, которые выравнивают один. Если есть хотя бы одно поле, которое не выравнивает 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, так что его поле i будет правильно выровнено. Вы также можете увидеть, что размер бара 6, а не 5. Аналогично, размер B_F (5 бара) равен 30, а не 25.
Итак, если вы жесткий код вместо sizeof(...)
, вы получите проблемы.
надеюсь, что это помогает.
все сводится к выравниванию памяти. Типичные 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 символа = 3 байта, 2 дюйма = 2*4 байта, так что 3 + 8)
к сожалению, из-за подклада структуру на самом деле занимает 12 байт. Это потому, что вы не можете поместить три символа и int в это 4-байтовое Слово, и поэтому там есть один байт заполненного пространства, который толкает int в его собственное слово. Это становится все более и более проблемой, чем более разнообразными становятся типы данных.
для ситуаций, когда такие вещи используются, и я не могу этого избежать, я пытаюсь сделать перерыв компиляции, когда предположения больше не сохраняются. Я использую что-то вроде следующего (или импульс.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)}
Я был бы в безопасности и заменил магическое число 3 на sizeof(foo)
Я думаю.
Я предполагаю, что код, оптимизированный для будущих архитектур процессоров, вероятно, представит некоторую форму заполнения.
и попытка отследить такую ошибку-настоящая боль!
как говорили другие, использование sizeof (foo) является более безопасной ставкой. Некоторые компиляторы (особенно эзотерические в embedded world) добавят в классы 4-байтовый заголовок. Другие могут выполнять фанковые трюки выравнивания памяти, в зависимости от настроек компилятора.
для основной платформы вы, вероятно, в порядке, но это не гарантия.
может возникнуть проблема с sizeof () при передаче данных между двумя компьютерами. На одном из них код может компилироваться с заполнением, а на другом без, и в этом случае sizeof() даст разные результаты. Если данные массива передаются с одного компьютера на другой, они будут неверно истолкованы, поскольку элементы массива не будут найдены там, где ожидалось. Одно из решений-убедиться, что #pragma pack (1) используется, когда это возможно, но этого может быть недостаточно для матрицы. Лучше всего предвидеть проблему и использовать заполнение до нескольких 8 байтов на элемент массива.