Константы перечисления ведут себя по-разному в C и C ++

81

Почему это:

#include <stdio.h>
#include <limits.h>
#include <inttypes.h>

int main() {
    enum en_e {
        en_e_foo,
        en_e_bar = UINT64_MAX,
    };
    enum en_e e = en_e_foo;
    printf("%zu\n", sizeof en_e_foo);
    printf("%zu\n", sizeof en_e_bar);
    printf("%zu\n", sizeof e);
}

печатать 4 8 8на C и 8 8 8на C ++ (на платформе с 4-х байтовыми вставками)?

У меня создалось впечатление, что UINT64_MAXприсвоение приведет к тому, что все константы перечисления будут иметь как минимум 64 бита, но en_e_fooостаются равными 32 в обычном C.

В чем причина несоответствия?

PSkocik
источник
1
Какие компиляторы? Не знаю, имеет ли это значение, но может.
Марк Рэнсом
@MarkRansom Это придумал gcc, но clang ведет себя так же.
PSkocik
3
«на платформе с 4-байтовыми целыми числами» . Не только платформа, но и компилятор определяет ширину типов. Возможно, это все. (Ответ Per Keith, на самом деле это не так, но имейте в
Гонки
1
@PSkocik: На самом деле это не изменение, просто этот вопрос нашел допустимое использование как c, так и c ++ (спрашивая, почему определенный код вызывает различное поведение между ними). Также нормально: спрашивать, как вызывать библиотеки C из C ++ и как писать C ++, который может вызываться из C. Очень не нормально: задавать вопрос C и вставлять тег C ++ на «чтобы он привлек больше внимания». Также не нормально: задать вопрос о C ++ и, как запоздалую мысль, «убедиться, что вы отвечаете и за C». (и для обычных жалобщиков - очень не нормально: изменение тега C ++ на тег C, потому что код использует функции, которые существуют в обоих стандартах)
Бен Фойгт

Ответы:

80

В C enumконстанта имеет тип int. В C ++ это перечислимый тип.

enum en_e{
    en_e_foo,
    en_e_bar=UINT64_MAX,
};

В C это нарушение ограничения , требующее диагностики ( при UINT64_MAX превышении INT_MAX, что очень вероятно). Компилятор AC может полностью отклонить программу или вывести предупреждение, а затем сгенерировать исполняемый файл, поведение которого не определено. (Не совсем ясно, что программа, которая нарушает ограничение, обязательно имеет неопределенное поведение, но в этом случае стандарт не говорит, каково это поведение, поэтому это все еще неопределенное поведение.)

gcc 6.2 об этом не предупреждает. лязг делает. Это ошибка в gcc; он неправильно подавляет некоторые диагностические сообщения при использовании макросов из стандартных заголовков. Спасибо Гжегожу Шпетковски за обнаружение отчета об ошибке: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71613

В C ++ каждый тип перечисления имеет базовый тип , который представляет собой некоторый целочисленный тип (не обязательно int). Этот базовый тип должен иметь возможность представлять все постоянные значения. Таким образом, в этом случае оба типа en_e_fooи en_e_barимеют en_eширину не менее 64 бита, даже если они intуже.

Кейт Томпсон
источник
10
быстрое примечание: для того, UINT64_MAXчтобы не превышать, INT_MAXтребуется intне менее 65 бит.
Ben Voigt
10
Действительно странно то, что gcc (5.3.1) выдает предупреждение с помощью -Wpedanticи, 18446744073709551615ULLно не с UINT64_MAX.
nwellnhof
4
@dascandy: Нет, это intдолжен быть подписанный тип, поэтому он должен быть не менее 65 бит, чтобы иметь возможность репрезентации UINT64_MAX(2 ** 64-1).
Кейт Томпсон
1
@KeithThompson, 6.7.2.2 говорит, что «идентификаторы в списке перечислителя объявлены как константы, которые имеют тип int и могут появляться везде, где это разрешено». Я понимаю, что константы, которые объявляет одно перечисление C, не используют тип перечисления, поэтому оттуда не составит большого труда сделать их разными типами (особенно если это реализовано как расширение стандарта).
zneak
2
@AndrewHenle: en_e_barне больше, чем перечисление, en_e_fooменьше. Переменная enum была больше самой большой константы.
Бен Войт
25

Этот код просто недопустим для C в первую очередь.

Раздел 6.7.2.2 как в C99, так и в C11 говорит, что:

Ограничения:

Выражение, которое определяет значение константы перечисления, должно быть целочисленным константным выражением, значение которого может быть представлено как int.

Диагностика компилятора является обязательной, поскольку это нарушение ограничения, см. 5.1.1.3:

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

Бен Фойгт
источник
23

В C , хотя a enumсчитается отдельным типом, сами счетчики всегда имеют тип int.

C11 - 6.7.2.2 Спецификаторы перечисления

3 Идентификаторы в списке перечислителя объявляются как константы, имеющие тип int ...

Таким образом, поведение, которое вы видите, является расширением компилятора.

Я бы сказал, что имеет смысл увеличивать размер одного из счетчиков только в том случае, если его значение слишком велико.


С другой стороны, в C ++ все перечислители имеют тип, в котором enumони объявлены.

Из-за этого размер каждого счетчика должен быть одинаковым. Таким образом, размер целого enumрасширен для хранения самого большого счетчика.

СвятойЧерныйКошка
источник
11
Это расширение компилятора, но невозможность создания диагностики является несоответствием.
Ben Voigt
16

Как указывали другие, код плохо сформирован (в C) из-за нарушения ограничений.

Существует ошибка GCC № 71613 (о которой сообщалось в июне 2016 г.), в которой говорится, что некоторые полезные предупреждения заглушаются макросами.

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

Текущий обходной путь может заключаться в добавлении макроса унарным +оператором:

enum en_e {
   en_e_foo,
   en_e_bar = +UINT64_MAX,
};

что дает ошибку компиляции на моей машине с GCC 4.9.2:

$ gcc -std=c11 -pedantic-errors -Wall main.c 
main.c: In function ‘main’:
main.c:9:20: error: ISO C restricts enumerator values to range ofint’ [-Wpedantic]
         en_e_bar = +UINT64_MAX
Гжегож Шпетковски
источник
12

C11 - 6.7.2.2/2

Выражение, определяющее значение константы перечисления, должно быть целочисленным константным выражением, значение которого может быть представлено как int.

en_e_bar=UINT64_MAXявляется нарушением ограничения, и это делает приведенный выше код недействительным. Должно появиться диагностическое сообщение, подтверждающее реализацию, как указано в черновике C11:

Соответствующая реализация должна выдать по крайней мере одно диагностическое сообщение (идентифицированное способом, определяемым реализацией), если блок трансляции предварительной обработки или блок трансляции содержит нарушение любого правила или ограничения синтаксиса, [...]

Похоже, что в GCC есть ошибка, и ему не удалось создать диагностическое сообщение. (Bug указывается в ответе по Гжегож Szpetkowski

хаки
источник
8
«неопределенное поведение» - это эффект времени выполнения. sizeofявляется оператором времени компиляции. Здесь нет UB, и даже если бы он был, это не могло бы повлиять sizeof.
Ben Voigt
2
Вы должны найти стандартную цитату, согласно которой перечислители, которые не могут поместиться в int, - это UB. Я очень скептически отношусь к этому заявлению, и мой голос останется твердым -1, пока это не прояснится.
zneak
3
@Sergey: Стандарт C действительно говорит: «Выражение, определяющее значение константы перечисления, должно быть целочисленным константным выражением, значение которого может быть представлено как int». но нарушение этого условия будет нарушением ограничения, требуется диагностика, а не UB.
Ben Voigt
3
@haccks: Да? Это нарушение ограничения, и «соответствующая реализация должна выдать по крайней мере одно диагностическое сообщение (идентифицированное способом, определяемым реализацией), если предварительно обрабатывающая единица трансляции или единица трансляции содержит нарушение любого синтаксического правила или ограничения, даже если поведение также явно задано как неопределенное или определенное реализацией ".
Ben Voigt
2
Есть разница между переполнением и усечением. Переполнение - это когда у вас есть арифметическая операция, которая дает значение, слишком большое для ожидаемого типа результата, а подписанное переполнение - UB. Усечение - это когда у вас есть значение, которое слишком велико для начала целевого типа (например, short s = 0xdeadbeef), и поведение определяется реализацией.
zneak
5

Я взглянул на стандарты, и моя программа, похоже, нарушает ограничения в C из-за 6.7.2.2p2 :

Ограничения: выражение, определяющее значение константы перечисления, должно быть целочисленным константным выражением, значение которого может быть представлено как int.

и определен в C ++ из-за 7.2.5:

Если базовый тип не фиксирован, тип каждого перечислителя является типом его инициализирующего значения: - Если для перечислителя указан инициализатор, инициализирующее значение имеет тот же тип, что и выражение, а константное выражение должно быть целым постоянное выражение (5.19). - Если для первого перечислителя не указан инициализатор, значение инициализации имеет неопределенный целочисленный тип. - В противном случае тип инициализирующего значения совпадает с типом инициализирующего значения предыдущего перечислителя, если только увеличиваемое значение не может быть представлено в этом типе, и в этом случае тип является неопределенным интегральным типом, достаточным для содержания увеличенного значения. Если такого типа не существует, программа плохо сформирована.

PSkocik
источник
3
Это не «неопределенное» в C, оно «плохо сформировано», потому что нарушено ограничение. Компилятор ДОЛЖЕН генерировать диагностику нарушения.
Ben Voigt
@BenVoigt Спасибо, что научили меня различать. Исправил это в ответе (который я сделал, потому что я пропустил цитату из стандарта C ++ в других ответах).
PSkocik