Enum-Konstanten verhalten sich in C und C ++ unterschiedlich

81

Warum macht das:

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

Drucken 4 8 8in C und 8 8 8in C ++ (auf einer Plattform mit 4 Byte Ints)?

Ich hatte den Eindruck, dass die UINT64_MAXZuweisung alle Aufzählungskonstanten auf mindestens 64 Bit zwingen würde, aber en_e_fooin Ebene C bei 32 bleibt.

Was ist der Grund für die Diskrepanz?

PSkocik
quelle
1
Welche Compiler? Ich weiß nicht, ob es einen Unterschied macht, aber es könnte sein.
Mark Ransom
@ MarkRansom Es kam mit gcc, aber Clang verhält sich genauso.
PSkocik
Live-Beispiel von C
Drew Dormann
3
"Auf einer Plattform mit 4 Byte Ints" Nicht nur die Plattform, sondern auch der Compiler bestimmt die Typbreiten. Das kann alles sein. (Per Keiths Antwort ist es eigentlich nicht, aber seien Sie sich solcher Möglichkeiten im Allgemeinen bewusst)
Leichtigkeitsrennen im Orbit
1
@PSkocik: Keine wirkliche Änderung, nur dass diese Frage eine gültige Verwendung von c und c ++ gefunden hat (die Frage, warum bestimmter Code ein unterschiedliches Verhalten zwischen den beiden verursacht). Ebenfalls in Ordnung: Fragen, wie man C-Bibliotheken aus C ++ aufruft und wie man C ++ schreibt, das aus C aufgerufen werden kann. Sehr nicht in Ordnung: eine C-Frage stellen und ein C ++ - Tag auf "damit es mehr Augäpfel bekommt" werfen. Auch nicht in Ordnung: Stellen Sie eine C ++ - Frage und stellen Sie nachträglich sicher, dass Sie auch für C antworten. (und für die üblichen Beschwerdeführer - sehr nicht in Ordnung: Ändern eines C ++ - Tags in ein C-Tag, da der Code Funktionen verwendet, die in beiden Standards vorhanden sind)
Ben Voigt

Antworten:

80

In C ist eine enumKonstante vom Typ int. In C ++ ist es vom Aufzählungstyp.

enum en_e{
    en_e_foo,
    en_e_bar=UINT64_MAX,
};

In C handelt es sich um eine Einschränkungsverletzung , für die eine Diagnose erforderlich ist ( wenn diese UINT64_MAX überschritten wird INT_MAX, was höchstwahrscheinlich der Fall ist). Der AC-Compiler kann das Programm insgesamt ablehnen oder eine Warnung ausgeben und dann eine ausführbare Datei generieren, deren Verhalten undefiniert ist. (Es ist nicht 100% klar, dass ein Programm, das gegen eine Einschränkung verstößt, notwendigerweise ein undefiniertes Verhalten aufweist. In diesem Fall gibt der Standard jedoch nicht an, wie das Verhalten ist. Das ist also immer noch undefiniertes Verhalten.)

gcc 6.2 warnt nicht davor. Klirren tut. Dies ist ein Fehler in gcc; Einige Diagnosemeldungen werden fälschlicherweise gesperrt, wenn Makros aus Standardheadern verwendet werden. Vielen Dank an Grzegorz Szpetkowski für das Auffinden des Fehlerberichts: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71613

In C ++ hat jeder Aufzählungstyp einen zugrunde liegenden Typ , der ein ganzzahliger Typ ist (nicht unbedingt int). Dieser zugrunde liegende Typ muss in der Lage sein, alle konstanten Werte darzustellen. In diesem Fall sind also beide en_e_foound en_e_barvom Typ en_e, der mindestens 64 Bit breit sein muss, auch wenn er intschmaler ist.

Keith Thompson
quelle
10
Kurzer Hinweis: Um UINT64_MAXnicht zu überschreiten INT_MAX, intsind mindestens 65 Bit erforderlich .
Ben Voigt
10
Das wirklich Seltsame ist, dass gcc (5.3.1) eine Warnung mit -Wpedanticund 18446744073709551615ULLaber nicht mit ausgibt UINT64_MAX.
Nwellnhof
4
@dascandy: Nein, intmuss ein vorzeichenbehafteter Typ sein, daher müssen mindestens 65 Bit vorhanden sein, um dargestellt werden zu können UINT64_MAX(2 ** 64-1).
Keith Thompson
1
@KeithThompson, 6.7.2.2 sagt, dass "die Bezeichner in einer Enumeratorliste als Konstanten deklariert sind, die den Typ int haben und dort erscheinen können, wo dies zulässig ist." Mein Verständnis ist, dass die Konstanten, die eine einzelne C-Enumeration deklariert, nicht den Typ der Enumeration verwenden. Daher ist es nicht sehr schwierig, sie zu unterschiedlichen Typen zu machen (insbesondere, wenn sie als Erweiterung des Standards implementiert sind).
Zneak
2
@ AndrewHenle: en_e_barist nicht größer als die Aufzählung, en_e_fooist kleiner. Die Enum-Variable war so groß wie die größte Konstante.
Ben Voigt
25

Dieser Code ist in erster Linie kein gültiges C.

In Abschnitt 6.7.2.2 in C99 und C11 heißt es:

Einschränkungen:

Der Ausdruck, der den Wert einer Aufzählungskonstante definiert, muss ein ganzzahliger Konstantenausdruck sein, dessen Wert als dargestellt werden kann int.

Eine Compilerdiagnose ist obligatorisch, da es sich um eine Einschränkungsverletzung handelt, siehe 5.1.1.3:

Eine konforme Implementierung muss mindestens eine Diagnosemeldung (auf implementierungsdefinierte Weise identifiziert) erzeugen, wenn eine vorverarbeitende Übersetzungseinheit oder Übersetzungseinheit eine Verletzung einer Syntaxregel oder -beschränkung enthält, selbst wenn das Verhalten auch explizit als undefiniert oder Implementierung angegeben ist. definiert.

Ben Voigt
quelle
23

Während in C a enumals separater Typ betrachtet wird, haben Enumeratoren selbst immer einen Typ int.

C11 - 6.7.2.2 Aufzählungsspezifizierer

3 Die Bezeichner in einer Enumeratorliste werden als Konstanten vom Typ int ...

Das angezeigte Verhalten ist also eine Compiler-Erweiterung.

Ich würde sagen, es ist sinnvoll, die Größe eines der Enumeratoren nur zu erweitern, wenn sein Wert zu groß ist.


Andererseits haben in C ++ alle Enumeratoren den Typ, in dem enumsie deklariert sind.

Aus diesem Grund muss die Größe jedes Enumerators gleich sein. Daher wird die Gesamtgröße enumerweitert, um den größten Enumerator zu speichern.

HolyBlackCat
quelle
11
Es ist eine Compiler-Erweiterung, aber das Versagen beim Generieren einer Diagnose ist eine Nichtkonformität.
Ben Voigt
16

Wie andere betonten, ist der Code (in C) aufgrund einer Einschränkungsverletzung schlecht geformt.

Es gibt den GCC-Fehler Nr. 71613 (gemeldet im Juni 2016), der besagt, dass einige nützliche Warnungen mit Makros zum Schweigen gebracht werden.

Nützliche Warnungen scheinen zum Schweigen gebracht zu werden, wenn Makros aus Systemheadern verwendet werden. Im folgenden Beispiel wäre beispielsweise eine Warnung für beide Aufzählungen nützlich, es wird jedoch nur eine Warnung angezeigt. Das gleiche kann wahrscheinlich auch für andere Warnungen passieren.

Die aktuelle Problemumgehung besteht möglicherweise darin, dem Makro einen unären +Operator voranzustellen :

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

Dies führt zu einem Kompilierungsfehler auf meinem Computer mit 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 of int [-Wpedantic]
         en_e_bar = +UINT64_MAX
Grzegorz Szpetkowski
quelle
12

C11 - 6.7.2.2/2

Der Ausdruck, der den Wert einer Aufzählungskonstante definiert, muss ein ganzzahliger Konstantenausdruck sein, dessen Wert als dargestellt werden kann int.

en_e_bar=UINT64_MAXist eine Einschränkungsverletzung und dies macht den obigen Code ungültig. Eine Diagnosemeldung sollte erstellt werden, indem die Implementierung gemäß dem C11-Entwurf bestätigt wird:

Eine konforme Implementierung muss mindestens eine Diagnosemeldung (auf implementierungsdefinierte Weise identifiziert) erzeugen, wenn eine vorverarbeitende Übersetzungseinheit oder Übersetzungseinheit eine Verletzung einer Syntaxregel oder -beschränkung enthält, [...]

Es scheint, dass GCC einen Fehler hat und die Diagnosemeldung nicht erstellt werden konnte. ( Fehler wird in der Antwort von Grzegorz Szpetkowski gezeigt

Haccks
quelle
8
"undefiniertes Verhalten" ist ein Laufzeiteffekt. sizeofist ein Operator zur Kompilierungszeit. Hier gibt es keine UB, und selbst wenn es eine gäbe, könnte dies keinen Einfluss haben sizeof.
Ben Voigt
2
Sie sollten das Standardzitat finden, dass Enumeranten, die nicht in ein int passen, UB sind. Ich bin sehr skeptisch gegenüber dieser Aussage und meine Stimme wird solide -1 bleiben, bis dies geklärt ist.
Zneak
3
@Sergey: Der C-Standard sagt tatsächlich: "Der Ausdruck, der den Wert einer Aufzählungskonstante definiert, muss ein Ausdruck einer ganzzahligen Konstante sein, dessen Wert als int darstellbar ist." Dies zu verletzen wäre jedoch eine Einschränkungsverletzung, eine Diagnose erforderlich, nicht UB.
Ben Voigt
3
@haccks: Ja? Es handelt sich um eine Einschränkungsverletzung. "Eine konforme Implementierung muss mindestens eine Diagnosemeldung (auf implementierungsdefinierte Weise identifiziert) erzeugen, wenn eine vorverarbeitende Übersetzungseinheit oder Übersetzungseinheit eine Verletzung einer Syntaxregel oder -einschränkung enthält, auch wenn das Verhalten ebenfalls vorliegt explizit als nicht definiert oder implementierungsdefiniert angegeben. "
Ben Voigt
2
Es gibt einen Unterschied zwischen Überlauf und Abschneiden. Überlauf ist, wenn Sie eine arithmetische Operation haben, die einen Wert erzeugt, der für den erwarteten Ergebnistyp zu groß ist, und der signierte Überlauf UB ist. Abschneiden ist, wenn Sie einen Wert haben, der für den Zieltyp zu groß war (wie short s = 0xdeadbeef), und das Verhalten durch die Implementierung definiert ist.
Zneak
5

Ich habe mir die Standards angesehen und mein Programm scheint aufgrund von 6.7.2.2p2 eine Einschränkungsverletzung in C zu sein :

Einschränkungen: Der Ausdruck, der den Wert einer Aufzählungskonstante definiert, muss ein ganzzahliger Konstantenausdruck sein, dessen Wert als int dargestellt werden kann.

und in C ++ aufgrund von 7.2.5 definiert:

Wenn der zugrunde liegende Typ nicht festgelegt ist, entspricht der Typ jedes Enumerators dem Typ seines Initialisierungswerts: - Wenn für einen Enumerator ein Initialisierer angegeben wird, hat der Initialisierungswert denselben Typ wie der Ausdruck, und der Konstantenausdruck muss ein Integral sein konstanter Ausdruck (5.19). - Wenn für den ersten Enumerator kein Initialisierer angegeben ist, hat der Initialisierungswert einen nicht angegebenen Integraltyp. - Andernfalls ist der Typ des Initialisierungswerts derselbe wie der Typ des Initialisierungswerts des vorhergehenden Enumerators, es sei denn, der inkrementierte Wert ist in diesem Typ nicht darstellbar. In diesem Fall ist der Typ ein nicht angegebener integraler Typ, der ausreicht, um den inkrementierten Wert zu enthalten. Wenn kein solcher Typ existiert, ist das Programm schlecht geformt.

PSkocik
quelle
3
Es ist nicht "undefiniert" in C, es ist "schlecht geformt", weil eine Einschränkung verletzt wird. Der Compiler MUSS eine Diagnose bezüglich des Verstoßes erstellen.
Ben Voigt
@ BenVoigt Danke, dass du mir den Unterschied beigebracht hast. Es wurde in der Antwort behoben (die ich gemacht habe, weil ich in den anderen Antworten ein Zitat aus dem C ++ - Standard verpasst habe).
PSkocik