2/02/2009

Перечислимые типы и строгая типизация.

Что такое перечислимый тип?
Перечислимый тип определяется как набор идентификаторов, с точки зрения языка играющих ту же роль, что и обычные именованные константы, но связанные с этим типом.
В языке Си, изначально перечислимый тип отсутствовал, но позднее был добавлен в стандарт ANSI.

Вот пример объявления перечислимого типа в языке Си:

typedef enum my_enum {
A, B, C
} my_enum_t;

Он состоит из трех элементов A,B,C.
Каждый элемент перечисления представляется с помощью типа int.
Фактически эта запись равносильна следующей (на большинстве платформ):

typedef int my_enum_t;
const my_enum_t A = 0;
const my_enum_t B = 1;
const my_enum_t C = 2;

Согласно стандарту ANSI C элементы перечисления по умолчанию нумеруются с 0.
Если какому-либо из элементов присвоить какое нибудь значение, то, все последующие элементы будут увеличиваться на 1, относительно этого значения.

Например:

typedef enum my_enum {
A = 500, B, C
} my_enum_t;

В таком перечислении значение A будет 500, B - 501, C - 502 и т.д.

К чему все это, спросите вы?
Пытаясь портировать одну небольшую и очень известную библиотеку,
на embeddedLinux для Архитектуры ARM9, я наткнулся на следующую проблему:
один и тот же код, скомпилированный компилятором одной и той же версии (gcc-4.1.1), с одними и теми же флагами отказывался работать.

Вот пример того кода, который не работал на ARM9:

1 #include <stdio.h>
2
3 typedef enum enum_A {
4 A1 = 0, A2, A3, A4
5 } enum_A_t;
6
7 typedef enum enum_B {
8 B1 = 1000000, B2, B3, B4, B5
9 } enum_B_t;
10
11 int main (int argc, char *argv[])
12 {
13 enum_A_t A;
14 enum_B_t B = B1;
15
16 A = B3;
17
18 printf ("A=%d, B=%d\n", sizeof (enum_A_t), sizeof (enum_B_t));
19 if (A > B)
20 printf ("Hello\n");
21 }

Ошибка состоит в том, что на x86 на консоль выводится слово HELLO, а на arm9 нет.
Как оказалось такую ситуацию можно повторить и на x86 архитектуре.

Где же ошибка:
На мой взгляд строки 16 и 20 - это просто надругательство над всем смыслом перечислимого типа. По моему мнению такое делать нельзя. Но компилятор gcc это прекрасно проглатывает, абсолютно никак не оповещя пользователя о возможных последствиях.

Ведь enum_A_t и enum_B_t это два абсолютно разных типа, с абсолютно разным диапазоном значений.
Такой код работает только потому, что элементы перечисления по сути являются элементами типа int.

Но что будет если gcc начнет оптимизировать?
Как оказалось, для экономии памяти на embedded платформе, в компиляторе была по-умолчанию включена опция -fshort-enums. При включении этой опции компилятор начинает оптимизировать перечисления, для того чтобы они занимали меньше памяти.
Как он это делает?

Просто берет и начинает использовать для хранения элементов перечисления не огромный 4-х байтовый целочисленный тип,а скажем однобайтовый char, или двухбайтовый short. Вот и в нашем случае, компилятор соптимизировал код так, что для хранения элементов типа enum_A_t использовался char, а для хранения элементов типа enum_B_t тип int.
Естественно сравнение в строке 19 всегда будет ложным.

Вывод.
Будьте внимательны при объявлении и использовании перечислений.
Такие ошибки трудноуловимы, и можно просидеть очень много времени в отладчике, пытаясь понять что же все таки не так.

Комментариев нет:

Отправить комментарий