Выравнивание элементов данных C++ и упаковка массивов

Во время обзора кода я наткнулся на некоторый код, который определяет простую структуру следующим образом:

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), и он работал на обеих.

Допустимы ли приведенные выше предположения? Если нет, то при каких условиях (например, изменение в ОС / компиляторе) они могут потерпеть неудачу?

9 ответов

Решение

Массив объектов должен быть смежным, поэтому между объектами никогда не бывает заполнения, хотя заполнение может быть добавлено к концу объекта (производя почти такой же эффект).

Учитывая, что вы работаете с символами, предположения чаще всего правильны, чем нет, но стандарт 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 от Bar) составляет 30, а не 25.

Так что, если вы жесткий код вместо sizeof(...)Вы получите проблему здесь.

Надеюсь это поможет.

Я был бы в безопасности и заменил магическое число 3 на sizeof(foo) Я считаю.

Я предполагаю, что код, оптимизированный для будущих архитектур процессоров, вероятно, представит некоторую форму заполнения.

И попытка отследить такого рода ошибку - настоящая боль!

В ситуациях, когда используются такие вещи, и я не могу этого избежать, я стараюсь прервать компиляцию, когда презумпции перестают действовать. Я использую что-то вроде следующего (или 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)}

Все сводится к выравниванию памяти. Типичные 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 байтов. Это потому, что вы не можете вписать три символа char и int в это 4-байтовое слово, и поэтому есть один байт отступа, который вставляет int в его собственное слово. Это становится все более и более сложной проблемой, чем разнообразнее становятся типы данных.

Как уже говорили другие, использование sizeof(foo) является более безопасной ставкой. Некоторые компиляторы (особенно эзотерические во встроенном мире) добавляют 4-байтовый заголовок к классам. Другие могут делать забавные трюки по выравниванию памяти, в зависимости от настроек вашего компилятора.

Для основной платформы вы, вероятно, в порядке, но это не гарантия.

При передаче данных между двумя компьютерами может возникнуть проблема с функцией sizeof(). На одном из них код может компилироваться с отступом, а на другом без него, в этом случае sizeof() даст другие результаты. Если данные массива передаются с одного компьютера на другой, они будут неправильно интерпретированы, поскольку элементы массива не будут найдены там, где ожидается. Одно из решений - убедиться, что #pragma pack(1) используется всякий раз, когда это возможно, но этого может быть недостаточно для массивов. Лучше всего предвидеть проблему и использовать заполнение, кратное 8 байтам на элемент массива.

Другие вопросы по тегам