Константи Enum поводяться по-різному в 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.

Яке обґрунтування розбіжностей?


1
Які компілятори? Не знаю, чи це щось змінює, але може.
Марк Ренсом

@MarkRansom Він придумав gcc, але clang поводиться так само.
PSkocik


3
"на платформі з 4 байтовими входами" Ширина типу визначається не просто платформою, а компілятором. Це може бути все це. (За відповіддю Кіта, насправді це не так, але майте на увазі такі можливості загалом)
Гонки легкості на орбіті

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, що дуже ймовірно). Компілятор змінного струму може взагалі відхилити програму, або він може надрукувати попередження, а потім створити виконуваний файл, поведінка якого невизначена. (Не на 100% зрозуміло, що програма, яка порушує обмеження, обов'язково має невизначену поведінку, але в цьому випадку стандарт не говорить про те, що таке поведінка, тому це все ще невизначена поведінка.)

gcc 6.2 не попереджає про це. clang робить. Це помилка в 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
Дійсно дивно, що ПКУ (5.3.1) видає попередження з -Wpedanticі 18446744073709551615ULLа не з UINT64_MAX.
nwellnhof

4
@dascandy: Ні, це intмає бути підписаний тип, тому для того, щоб мати змогу подати запит UINT64_MAX(2 ** 64-1) , воно повинно мати принаймні 65 біт .
Кіт Томпсон,

1
@KeithThompson, 6.7.2.2, говорить, що "ідентифікатори у списку перечислювача оголошуються як константи, що мають тип int, і можуть з'являтися там, де такі дозволені". Я розумію, що константи, які декларує один перелік С, не використовують тип переліку, тому звідти не дуже велике розтягування, щоб зробити їх різними типами (особливо якщо це реалізовано як розширення до стандарту).
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 , хоча а enumвважається окремим типом, перечислювачі самі завжди мають тип int.

C11 - 6.7.2.2 Специфікатори перерахування

3 Ідентифікатори у списку перелічувача оголошуються як константи типу int ...

Таким чином, поведінка, яку ви бачите, є розширенням компілятора.

Я б сказав, що має сенс розширювати розмір одного з перелічувачів, лише якщо його значення занадто велике.


З іншого боку, в С ++ всі перечислювачі мають тип enumоголошеного.

Через це розмір кожного перелічувача повинен бути однаковим. Отже, розмір цілого enumрозширений, щоб зберігати найбільший перерахувач.


11
Це розширення компілятора, але невдача генерувати діагностику є невідповідністю.
Бен Войгт,

16

Як зазначали інші, код неправильно сформований (на С) через порушення обмежень.

Існує помилка 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є порушенням обмеження, і це робить наведений код недійсним. Діагностичне повідомлення повинно надходити шляхом підтвердження впровадження, як зазначено в проекті С11:

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

Здається, у GCC є якась помилка, і вона не змогла надати діагностичне повідомлення. (Bug вказується в відповіді по Гжегож Szpetkowski


8
"невизначена поведінка" - це ефект виконання. sizeofє оператором компіляції. Тут немає УБ, і навіть якщо б вони були, це не могло вплинути sizeof.
Бен Войгт

2
Ви повинні знайти стандартну цитату, що перелічувачі, які не можуть поміститися в int, є UB. Я дуже скептично ставлюсь до цієї заяви, і мій голос залишатиметься твердим -1, поки це не проясниться.
zneak

3
@Sergey: Стандарт C насправді каже: "Вираз, що визначає значення константи перерахування, повинен бути цілим виразом константи, значення якого можна представити як int". але порушення цього буде порушенням обмеження, потрібне діагностичне дослідження, а не UB.
Бен Войгт,

3
@haccks: Так? Це порушення обмеження, і "Відповідна реалізація повинна видавати принаймні одне діагностичне повідомлення (ідентифіковане визначеним реалізацією), якщо блок попередньої обробки або блок перекладу містить порушення будь-якого правила синтаксису або обмеження, навіть якщо поведінка також явно вказаний як невизначений або визначений реалізацією ".
Бен Войгт

2
Існує різниця між переповненням та скороченням. Переповнення - це коли у вас арифметична операція, яка створює значення, завелике для очікуваного типу результату, а підписане переповнення - UB. Зрізання - це коли у вас є значення, яке було занадто великим для цільового типу, щоб почати (як short s = 0xdeadbeef), і поведінка визначається реалізацією.
zneak

5

Я подивився стандарти, і моя програма виявляється порушенням обмежень в C через 6.7.2.2p2 :

Обмеження: Вираз, що визначає значення константи перерахування, повинен бути цілим виразом константи, що має значення, яке можна представити як int.

і визначено в C ++ через 7.2.5:

Якщо базовий тип не є фіксованим, типом кожного перечислювача є тип його ініціалізуючого значення: - Якщо для перечислювача вказано ініціалізатор, значення ініціалізуючого має той самий тип, що і вираз, а вираз константи повинен бути інтегралом постійний вираз (5.19). - Якщо для першого перечислювача не вказано ініціалізатор, значення ініціалізації має невизначений інтегральний тип. - В іншому випадку тип ініціалізуючого значення збігається з типом ініціалізуючого значення попереднього перечислювача, якщо збільшене значення не є репрезентативним у цьому типі, і в цьому випадку тип є невизначеним цілим типом, достатнім для того, щоб містити збільшене значення. Якщо такого типу не існує, програма неправильно сформована.


3
Це не "невизначено" в C, воно "неправильно сформовано", оскільки обмеження порушено. Компілятор ПОВИНЕН сформувати діагностику щодо порушення.
Бен Войгт

@BenVoigt Дякуємо, що навчили мене про різницю. Виправлено у відповіді (яку я зробив, оскільки в інших відповідях я пропустив цитату зі стандарту C ++).
PSkocik
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.