C – возможность выравнивания типа

Я пишу очень маленький интерпретатор для очень простого языка, который позволяет использовать простые определения структуры (из других структур и простых типов, таких как int, char, float, double и т. Д.). Я хочу, чтобы поля использовались как можно меньше, поэтому использование max_align_t или что-то подобное не может быть и речи. Теперь, интересно, есть ли лучший способ получить выравнивание любого другого типа, кроме этого:

#include  #include  #define GA(type, name) struct GA_##name { char c; type d; }; \ const unsigned int alignment_for_##name = offsetof(struct GA_##name, d); GA(int, int); GA(short, short); GA(char, char); GA(float, float); GA(double, double); GA(char*, char_ptr); GA(void*, void_ptr); #define GP(type, name) printf("alignment of "#name" is: %dn", alignment_for_##name); int main() { GP(int, int); GP(short, short); GP(char, char); GP(float, float); GP(double, double); GP(char*, char_ptr); GP(void*, void_ptr); } 

Это работает, но, может быть, есть что-то приятнее?

Это, вероятно, не очень портативно, но GCC принимает следующее:

 #define alignof(type) offsetof(struct { char c; type d; }, d) 

EDIT: И в соответствии с этим ответом , C разрешает кастинг для анонимных типов структуры (хотя я бы хотел, чтобы это заявление было подкреплено). Итак, следующее должно быть переносимым:

 #define alignof(type) ((size_t)&((struct { char c; type d; } *)0)->d) 

Другой подход с использованием выражений оператора GNU :

 #define alignof(type) ({ \ struct s { char c; type d; }; \ offsetof(struct s, d); \ }) 

В C11 добавляется _Alignof :

 printf("Alignment of int: %zu\n", _Alignof(int)); 

Обычно лучше использовать стиль и использовать alignof :

 #include  printf("Alignment of int: %zu\n", alignof(int)); 

Вы можете проверить C11 таким образом:

 #if __STDC_VERSION__ >= 201112L /* C11 */ #else /* not C11 */ #endif 

Если вы используете GCC или CLang, вы можете скомпилировать свой код в режиме C11, добавив -std=c11 (или -std=gnu11 если вы также хотите расширения GNU). Режим по умолчанию – gnu89 для GCC и gnu99 для CLang.


Обновить:

Возможно, вам не понадобится проверять выравнивание для вашей системы вообще, если вы сделаете несколько образованных догадок. Я бы рекомендовал использовать один из этих двух заказов:

 // non-embedded use long double, long long, void (*)(void), void*, double, long, float, int, short, char // embedded use (microcontrollers) long double, long long, double, long, float, void (*)(void), void*, int, short, char 

Это упорядочение отлично переносимо (но не всегда оптимально), так как в худшем случае вы получаете больше дополнений, чем в противном случае.

Далее следует (по общему признанию, длинное) обоснование. Не стесняйтесь пропустить все мимо этого момента, если вам все равно, как я пришел к такому выводу для заказа.


Покрытие большинства случаев

Это справедливо в C (независимо от реализации):

 // for both `signed` and `unsigned` sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long) sizeof(float) <= sizeof(double) <= sizeof(long double) 

Если вы немного поработаете с порядком, вы должны иметь возможность получать неуправляемые структуры в большинстве случаев. Обратите внимание, что это не гарантирует, что структура будет неудовлетворенной; но это будет в большинстве случаев в реальном мире , и это должно быть GoodEnough ™.

Следующее - это рекомендации, специфичные для реализации, но в большинстве случаев должны охватывать выравнивание.

Это, однако, прекрасно переносится повсюду (даже если это не оптимально) - до тех пор, пока вы согласитесь, что может быть какое-то дополнение (другими словами, не предполагайте, что структура не имеет прописных букв). Если вы ошибаетесь, все, что вы получите, это более крупная структура, поэтому нет никакой опасности столкнуться с неопределенным поведением любого рода.

То, что вы должны сделать, это упорядочить их от самых маленьких до самых маленьких, поскольку их выравнивание также будет в этом порядке. Предполагая типичный компилятор amd64:

 long long a; // 8-byte long b; // 8-byte or 4-byte; already aligned in both cases int c; // 4-byte; already aligned short d, e; // 2-byte; both already aligned char f; // 1-byte; always aligned 

Целочисленные типы

Итак, давайте начнем выяснять наш заказ, начиная с целых типов:

 long long, long, int, short, char 

Типы с плавающей точкой

Теперь типы с плавающей запятой. Что вы делаете с double ? Его выравнивание обычно составляет 8 байтов на 64-битных архитектурах и 4 байта на 32-битных (но в некоторых случаях это может быть 8-байтовое).

long long всегда не менее 8 байтов (это неявно требуется стандартом из-за его минимального диапазона), а long всегда составляет не менее 4 байтов (но обычно это 8-байтовое число в 64-битных, есть исключения, такие как Windows).

То, что я сделаю, будет поставлено между ними double . Обратите внимание, что double может иметь размер 4 байта (обычно во встроенных системах, таких как AVR / Arduino), но они практически всегда имеют long в 4 байта.

long double - сложный случай. Его выравнивание может варьироваться от 4 байтов (скажем, x86 Linux) до 16 байтов (amd64 Linux). Тем не менее, 4-байтовое выравнивание является историческим артефактом и является субоптимальным; поэтому я предполагаю, что он по крайней мере 8-байтовый и поставил его выше long long . Это также сделает его оптимальным, если его выравнивание составляет 16 байт.

Это оставляет float , который практически всегда составляет 4 байта, с 4-байтным выравниванием; Я поставлю его между long , что гарантируется как минимум 4 байта, а int , который может (обычно) быть 4 или 2 байта.

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

 long double, long long, double, long, float, int, short, char 

Типы указателей

Все, что у нас осталось, - это типы указателей. Размер разных указателей не обязательно одинаковый, но я собираюсь предположить, что это (и это верно в подавляющем большинстве случаев, если не во всех). Я предполагаю, что указатели на функции могут быть больше (подумайте об аппаратной архитектуре с большим ROM, чем ОЗУ), поэтому я поставлю их выше других.

Наихудший практический сценарий заключается в том, что они одинаковы, поэтому я ничего не добился; Лучшим случаем является то, что я удалил еще несколько дополнений.

Но как насчет размера? Обычно это относится к не встроенным системам:

 sizeof(long) <= sizeof(T*) <= sizeof(long long) 

В большинстве систем sizeof(long) и sizeof(T*) одинаковы; но, например, 64-битная Windows имеет 32-битную long и 64-бит T* . Однако во встроенных системах это другое; указатели могут быть 16-битными, что означает:

 sizeof(int) <= sizeof(T*) <= sizeof(long) 

Что делать с вами - это тот, кто знает, где это будет обычно. С одной стороны, оптимизация для внедренных, где первичное использование - нет, означает оптимизацию для необычного случая. С другой стороны, во встроенных системах память более ограничена, чем нет. Лично я бы рекомендовал оптимизацию для использования на рабочем столе, если только вы не создаете встроенное приложение. Поскольку выравнивание double обычно совпадает с размером указателя, но может быть больше , я бы поставил это ниже double .

 // non-embedded long double, long long, void (*)(void), void*, double, long, float, int, short, char 

Для встроенных применений я бы разместил его ниже float , поскольку выравнивание float обычно 4 байта, но T* - 2-байтовое или 4-байтовое:

 // embedded long double, long long, double, long, float, void (*)(void), void*, int, short, char