Выравнивание элементов данных 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++, безусловно, не гарантирует этого. Другой компилятор или даже просто изменение флагов, переданных вашему текущему компилятору, может привести к вставке заполнения между элементы структуры или следующие за последним элементом структуры,или оба.


Это определенно было бы безопаснее сделать:

sizeof(foo) * SOME_NUM

Если вы копируете свой массив таким образом, вы должны использовать

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 байтов на элемент массива.