Stałe wyliczeniowe zachowują się inaczej w C i C ++

81

Dlaczego to:

#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);
}

drukować 4 8 8w C i 8 8 8C ++ (na platformie z 4-bajtowymi liczbami całkowitymi)?

Miałem wrażenie, że UINT64_MAXprzypisanie wymusi na wszystkich stałych wyliczeniowych co najmniej 64 bity, ale en_e_foopozostanie na 32 w zwykłym C.

Jakie jest uzasadnienie tej rozbieżności?

PSkocik
źródło
1
Które kompilatory? Nie wiem, czy to coś zmienia, ale może.
Mark Ransom
@MarkRansom Pojawił się z gcc, ale clang zachowuje się tak samo.
PSkocik
Przykład na żywo C
Drew Dormann
3
„na platformie z 4-bajtowymi liczbami int” Nie tylko platforma, ale także kompilator określa szerokości typów. To może być wszystko, czym jest. (Według odpowiedzi Keitha, tak naprawdę nie jest, ale ogólnie uważaj na takie możliwości)
Lightness Races in Orbit
1
@PSkocik: Niezupełnie zmiana, tylko to, że to pytanie znalazło prawidłowe użycie zarówno c, jak i c ++ (pytanie, dlaczego określony kod powoduje różne zachowanie między nimi). Również ok: pytanie, jak wywołać biblioteki C z C ++ i jak napisać C ++, które można wywołać z C. Bardzo nie ok: zadawanie pytania w C i rzucanie tagu C ++ na "aby uzyskać więcej oczu". Również nie w porządku: zadawanie pytania w C ++ i po namyśle „upewnij się, że odpowiadasz również dla C”. (i dla zwykłych Complainers - bardzo nie OK: Zmienianie C ++ tag tag C, ponieważ wykorzystywany jest kod funkcji, które istnieją w obu standardach)
Ben Voigt

Odpowiedzi:

80

W C enumstała jest typu int. W C ++ jest to typ wyliczeniowy.

enum en_e{
    en_e_foo,
    en_e_bar=UINT64_MAX,
};

W C jest to naruszenie ograniczenia , wymagające diagnostyki ( jeśli UINT64_MAX przekracza INT_MAX, co najprawdopodobniej tak się dzieje). Kompilator AC może całkowicie odrzucić program lub może wydrukować ostrzeżenie, a następnie wygenerować plik wykonywalny, którego zachowanie jest nieokreślone. (Nie jest w 100% jasne, że program, który narusza ograniczenie, musi koniecznie mieć nieokreślone zachowanie, ale w tym przypadku standard nie mówi, jakie to zachowanie, więc jest to nadal niezdefiniowane zachowanie.)

gcc 6.2 nie ostrzega o tym. brzęk robi. To jest błąd w gcc; niepoprawnie blokuje niektóre komunikaty diagnostyczne, gdy używane są makra ze standardowych nagłówków. Podziękowania dla Grzegorza Szpetkowskiego za zlokalizowanie raportu błędu: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71613

W C ++ każdy typ wyliczenia ma typ bazowy , który jest typem całkowitym (niekoniecznie int). Ten typ bazowy musi być w stanie reprezentować wszystkie wartości stałe. W tym przypadku oba typy en_e_fooi en_e_barsą typu en_e, który musi mieć co najmniej 64 bity szerokości, nawet jeśli intjest węższy.

Keith Thompson
źródło
10
krótka uwaga: aby UINT64_MAXnie przekroczyć INT_MAXwymaga intco najmniej 65 bitów.
Ben Voigt
10
Naprawdę dziwne jest to, że gcc (5.3.1) emituje ostrzeżenie z -Wpedantica 18446744073709551615ULL, ale nie z UINT64_MAX.
nwellnhof
4
@dascandy: Nie, intmusi być typem ze znakiem, więc aby móc reprezentować, musiałby mieć co najmniej 65 bitów UINT64_MAX(2 ** 64-1).
Keith Thompson
1
@KeithThompson, 6.7.2.2 mówi, że „identyfikatory na liście modułów wyliczających są zadeklarowane jako stałe typu int i mogą pojawiać się wszędzie tam, gdzie takie są dozwolone”. Rozumiem, że stałe, które deklaruje pojedyncze wyliczenie C, nie używają typu wyliczenia, więc stamtąd nie ma dużego rozciągnięcia, aby uczynić je różnymi typami (zwłaszcza jeśli jest zaimplementowane jako rozszerzenie standardu).
zneak
2
@AndrewHenle: en_e_barnie jest większa niż wyliczenie, en_e_foojest mniejsza. Zmienna wyliczeniowa była tak duża, jak największa stała.
Ben Voigt
25

Ten kod po prostu nie jest prawidłowy w C w pierwszej kolejności.

Sekcja 6.7.2.2 w C99 i C11 mówi, że:

Ograniczenia:

Wyrażenie, które definiuje wartość stałej wyliczenia, powinno być wyrażeniem stałym będącym liczbą całkowitą, którego wartość można przedstawić jako int.

Diagnostyka kompilatora jest obowiązkowa, ponieważ stanowi naruszenie ograniczenia, patrz 5.1.1.3:

Zgodna implementacja powinna wygenerować co najmniej jeden komunikat diagnostyczny (zidentyfikowany w sposób zdefiniowany w implementacji), jeśli jednostka translacyjna lub jednostka translacyjna przetwarzania wstępnego zawiera naruszenie jakiejkolwiek reguły składniowej lub ograniczenia, nawet jeśli zachowanie jest również wyraźnie określone jako nieokreślone lub implementacja zdefiniowane.

Ben Voigt
źródło
23

W C , podczas gdy a enumjest uważany za oddzielny typ, same moduły wyliczające zawsze mają typ int.

C11 - 6.7.2.2 Specyfikatory wyliczenia

3 Identyfikatory na liście modułów wyliczających są zadeklarowane jako stałe, które mają typ int ...

Zatem zachowanie, które widzisz, jest rozszerzeniem kompilatora.

Powiedziałbym, że rozszerzenie rozmiaru jednego z modułów wyliczających ma sens tylko wtedy, gdy jego wartość jest zbyt duża.


Z drugiej strony w C ++ wszystkie moduły wyliczające mają typ tego, w enumjakim zostały zadeklarowane.

Z tego powodu rozmiar każdego modułu wyliczającego musi być taki sam. Tak więc rozmiar całości enumjest rozszerzany, aby przechowywać największy moduł wyliczający.

HolyBlackCat
źródło
11
Jest to rozszerzenie kompilatora, ale niepowodzenie w wygenerowaniu diagnostyki jest niezgodnością.
Ben Voigt
16

Jak wskazywali inni, kod jest źle sformułowany (w C) z powodu naruszenia ograniczeń.

Istnieje błąd GCC # 71613 (zgłoszony w czerwcu 2016 r.), Który stwierdza, że ​​niektóre przydatne ostrzeżenia są wyciszane za pomocą makr.

Przydatne ostrzeżenia wydają się być wyciszane, gdy używane są makra z nagłówków systemowych. Na przykład w poniższym przykładzie ostrzeżenie byłoby przydatne dla obu wyliczeń, ale jest wyświetlane tylko jedno ostrzeżenie. To samo może się prawdopodobnie zdarzyć w przypadku innych ostrzeżeń.

Obecnym rozwiązaniem może być dołączenie makra +operatorem jednoargumentowym :

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

co powoduje błąd kompilacji na moim komputerze z 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
Grzegorz Szpetkowski
źródło
12

C11 - 6.7.2.2/2

Wyrażenie, które definiuje wartość stałej wyliczenia, powinno być wyrażeniem stałym będącym liczbą całkowitą, które ma wartość, którą można przedstawić jako int.

en_e_bar=UINT64_MAXjest naruszeniem ograniczenia, co powoduje, że powyższy kod jest nieprawidłowy. Komunikat diagnostyczny powinien zostać wygenerowany poprzez potwierdzenie implementacji zgodnie z projektem C11:

Zgodna implementacja powinna generować co najmniej jeden komunikat diagnostyczny (zidentyfikowany w sposób zdefiniowany w implementacji), jeśli jednostka translacyjna lub jednostka translacyjna przetwarzająca wstępnie zawiera naruszenie jakiejkolwiek reguły lub ograniczenia składni, [...]

Wygląda na to, że GCC ma jakiś błąd i nie udało mu się wygenerować komunikatu diagnostycznego. (Bug jest skierowany w odpowiedzi przez Grzegorza Szpetkowski

haccks
źródło
8
„niezdefiniowane zachowanie” jest efektem działania. sizeofjest operatorem czasu kompilacji. Nie ma tutaj UB, a nawet gdyby istniał, nie miałoby to wpływu sizeof.
Ben Voigt
2
Powinieneś znaleźć standardowy cytat, że wyliczenia, które nie mieszczą się w int, to UB. Jestem bardzo sceptyczny wobec tego stwierdzenia i mój głos pozostanie solidnym -1, dopóki nie zostanie to wyjaśnione.
zneak
3
@Sergey: Standard C faktycznie mówi: „Wyrażenie, które definiuje wartość stałej wyliczenia, powinno być wyrażeniem stałym będącym liczbą całkowitą, które ma wartość reprezentowalną jako liczba całkowita”. ale naruszenie tego byłoby naruszeniem ograniczenia, wymaganej diagnostyki, a nie UB.
Ben Voigt
3
@haccks: Tak? Jest to naruszenie ograniczenia, a „Zgodna implementacja powinna wygenerować co najmniej jeden komunikat diagnostyczny (zidentyfikowany w sposób zdefiniowany w implementacji), jeśli jednostka tłumacząca lub jednostka tłumacząca przetwarzająca wstępnie zawiera naruszenie jakiejkolwiek reguły składniowej lub ograniczenia, nawet jeśli zachowanie jest również wyraźnie określony jako niezdefiniowany lub niezdefiniowany w implementacji. "
Ben Voigt
2
Istnieje różnica między przepełnieniem a obcięciem. Przepełnienie występuje, gdy masz operację arytmetyczną, która generuje wartość zbyt dużą dla oczekiwanego typu wyniku, a przepełnienie ze znakiem to UB. Obcięcie ma miejsce, gdy masz wartość, która była zbyt duża, aby typ docelowy zaczynał się od (np. short s = 0xdeadbeef), A zachowanie jest zdefiniowane przez implementację.
zneak
5

Przyjrzałem się standardom i mój program wydaje się naruszać ograniczenia w C z powodu 6.7.2.2p2 :

Ograniczenia: Wyrażenie, które definiuje wartość stałej wyliczenia, powinno być wyrażeniem stałym w postaci liczby całkowitej, którego wartość można przedstawić jako liczbę całkowitą.

i zdefiniowany w C ++ z powodu 7.2.5:

Jeśli typ bazowy nie jest ustalony, typ każdego modułu wyliczającego jest typem jego wartości inicjującej: - Jeśli dla modułu wyliczającego określono inicjalizator, wartość inicjująca ma ten sam typ co wyrażenie, a wyrażenie stałe powinno być całką stała ekspresja (5.19). - Jeśli dla pierwszego modułu wyliczającego nie określono inicjatora, wartość inicjująca ma nieokreślony typ całkowity. - W przeciwnym razie typ wartości inicjującej jest taki sam, jak typ wartości inicjującej poprzedniego modułu wyliczającego, chyba że wartość zwiększona nie jest reprezentowalna w tym typie, w takim przypadku typ jest nieokreślonym typem całkowitym wystarczającym do zawarcia zwiększonej wartości. Jeśli nie ma takiego typu, program jest źle sformułowany.

PSkocik
źródło
3
Nie jest „nieokreślony” w C, jest „źle sformułowany”, ponieważ zostało naruszone ograniczenie. Kompilator MUSI wygenerować diagnostykę dotyczącą naruszenia.
Ben Voigt
@BenVoigt Dzięki za nauczenie mnie różnicy. Poprawiłem to w odpowiedzi (którą zrobiłem, ponieważ w innych odpowiedziach przegapiłem cytat ze standardu C ++).
PSkocik