enum-Datentyp
Der enumerated Datentyp (im Folgenden vereinfacht enum genannt) ist ein anwenderdefinierter Datentyp, der eingesetzt wird, um mehrere logisch zusammengehörige Symbole (die Enumeratoren) zu definieren. Es gibt zwei Arten von enums: den scoped enum und den unscoped enum.
scoped enum
Die enum-Anweisung für ein scoped enum hat folgende Syntax:x
enum class ETYP [:DTYP]
{E1[=VAL1],E2[=VAL2],... } [EOBJ1,...];
Nach den Schlüsselwörtern enum class folgt ein eindeutiger Name ETYP, der den anwenderdefinierten enum-Datentyp benennt.
Im Anschluss daran folgen optional ein Doppelpunkt und der dem enum zugrunde liegende Datentyp DTYP. Dieser kann ein beliebiger Zeichen- oder Integer-Datentyp sein, jedoch muss er in der Lage sein die internen Werte für die Enumeratoren abzubilden. Standardmäßig wird der 'kleinste' Integer-Datentyp verwendet, in dem die Werte der Enumeratoren dargestellt werden können.
Innerhalb der geschweiften Klammern folgen die symbolischen Namen der Enumeratoren Ex.
Nach der schließenden geschweiften Klammer können optional Objekte des enum-Datentyps definiert werden.
// enum-Datentyp definieren und gleichzeitig
// ein enum-Objekt definieren
enum class Colors
{
RED, YELLOW, GREEN, BLUE
} myColors;
// weitere enum-Objekte des selben Typs definieren
Colors color1{Colors::RED}, color2;
Im Beispiel wird der enum-Datentyp Colors, mit den Enumeratoren RED, YELLOW, GREEN und BLUE sowie das enum-Objekt myColors definiert. Im Anschluss daran werden zwei weitere Objekte color1 und color2 vom gleichen enum-Datentyp definiert, wobei color1 mit dem Enumerator RED initialisiert wird.
Einem enum-Objekt können nur die innerhalb des enum-Blocks definierten Enumeratoren zugewiesen werden. Dabei ist der Enumerator voll zu qualifizieren, d.h vor dem Enumerator ist der enum-Datentyp gefolgt von zwei Doppelpunkten anzugeben. Der Grund hierfür ist, dass unterschiedliche enum-Datentypen gleichnamige Enumeratoren enthalten können, ohne dass dies zu einer Doppeldeutigkeit führt.
Das nachfolgende Beispiel definiert eine enum-Variable s1, die die Zustände eines Schalters abbildet.
// enum Definition
enum class Switch {OFF, LOW, HIGH};
...
// Definition eines Schalters
Switch s1;
s1 = Switch::LOW;
// ...weitere Anweisungen
// Pruefen ob Schalter im Zustand LOW ist
if (s1 == Switch::LOW)
s1 = Switch::HIGH; // Schalter auf HIGH
Werden in einem Block {...} häufig Zugriffe auf die Enumeratoren eines scoped enum benötigt, können mithilfe der Anweisung
using enum ETYP;
die Enumeratoren des enum-Typs ETYP eingeblendet werden. In diesem Fall können die Enumeratoren ohne volle Qualifizierung verwendet werden.
#include <print>
// enum Definition
enum class Switch {OFF, LOW, HIGH};
int main()
{
// Definition eines Schalters
Switch s1;
s1 = Switch::LOW;
// ...weitere Anweisungen
switch (s1)
{
using enum Switch;
case OFF:
s1 = LOW;
std::println("Schalter: LOW");
break;
case LOW:
s1 = HIGH;
std::println("Schalter: HIGH");
break;
case HIGH:
s1 = OFF;
std::println("Schalter: OFF");
break;
}
}
Schalter: HIGH
unscoped enum
Diese Form der enum-Anweisung gleicht bis auf das fehlende Schlüsselwort class der ersten Form:
enum [ETYP] [:DTYP]
{E1[=VAL1],E2[=VAL2],... }[EOBJ1,...];
unscoped enums sind nicht typsicher, d.h., besitzen unterschiedliche unscoped enums gleichnamige Enumeratoren, führt dies zu einem Fehler.
Der 'Vorteil' dafür ist, dass der Zugriff auf die Enumeratoren durch alleinig Angabe des Enumerators erfolgt, d.h es ist keine volle Qualifizierung notwendig.
// unscoped enum definieren
enum Style {BOLD, ITALIC, UNDERLINE};
// enum-Variable mit Initialsierung
Style myStyle{UNDERLINE};
...
// Zuweisung an enum-Variable
myStyle = BOLD;
In realen Anwendungen sollten unscoped enums nur innerhalb von Klassen einsetzen werden, um Konflikte mit gleichnamigen Enumeratoren zu vermeiden (siehe dazu Kapitel Klassen).
Werte der Enumeratoren
Die internen Werte der Enumeratoren werden standardmäßig fortlaufend inkrementiert, wobei der erste Enumerator den Wert 0 besitzt. Soll einem Enumerator explizit ein Wert zugewiesen werden, folgen nach dem Enumerator der Zuweisungsoperator und der Wert.
enum class Schalter {OFF=1, LOW, HIGH=LOW+10};
Alle nach einer Zuweisung folgende Enumeratoren werden, wenn ihnen nicht erneut ein Wert zugewiesen wird, wieder fortlaufend um eins erhöht. Ein in der Liste definierter Enumerator kann zur Berechnung des Wertes eines nachfolgenden Enumerators verwendet werden. Im Beispiel oben besitzt der Enumerator OFF den Wert 1, LOW den Wert 2 und HIGH den Wert 12.
Mithilfe der in der Header-Datei utility definierten Bibliotheksfunktion
std::to_underlying(e)
kann der Enumerator e eines enums in seinen Integer-Wert konvertiert werden, um damit z.B. Rechenoperationen durchzuführen. Dabei ist zu beachten, dass das Ergebnis der Rechenoperation aber ebenfalls ein Integer-Wert ist. So gibt das nachfolgende Beispiel als Ergebnis den Wert 3 aus.
#include <iostream>
#include <utility>
// scoped enum definieren
enum class Style {BOLD=1, ITALIC=2, UNDERLINE=4};
// enum-Variable mit Initialsierung
Style myStyle;
int main()
{
auto var = std::to_underlying(Style::BOLD) |
std::to_underlying(Style::ITALIC);
std::cout << var;
}
3
Fehlerfallen
Mit einem enum-Objekt können keine Rechenoperationen durchgeführt werden, d.h., die in der for-Schleife angegebene Anweisung s1++ führt zu einem Fehler.
// Definition
enum class Switch {OFF, LOW, HIGH} s1;
...
// enum in einer while Schleife ist ok
s1=Switch::LOW;
while (s1 != Switch::OFF)
{
// ...weitere Anweisungen
}
// Aber das geht nicht!
for(s1=Switch::OFF; s1!=Switch::HIGH; s1++)
{
//...weitere Anweisungen
}
Eine weitere Fehlerfalle lauert bei der Übergabe eines enums an eine Funktion. Im nachfolgenden Beispiel wird der enum-Datentyp Style zur Definition von verschiedenen Stilen definiert. An die Funktion Func() soll ein Datum dieses enum-Datentyps übergeben werden.
// Enum-Definition
enum class Style {STYLE1, STYLE2, STYLE3};
// Funktionsdefinition
void Func(Style newStyle)
{
//...weitere Anweisungen
}
int main()
{
// So geht's
Func(Style::STYLE2);
// Aber das erzeugt einen Fehler
Func(Style::STYLE1 | Style::STYLE2);
}
Wird die Funktion mit einem Enumerator aufgerufen, ist alles in Ordnung (erster Funktionsaufruf). Werden jedoch mehrere Enumeratoren beim Aufruf der Funktion z.B. verodert, meldet der Compiler beim Übersetzen einen Fehler (zweiter Funktionsaufruf). Der Grund dafür ist prinzipiell der gleiche wie im vorherigen Fall: mit den Enumeratoren können standardmäßig keine Rechenoperationen durchgeführt werden.
Wenn arithmetische Operationen mit enums durchgeführt werden sollen, müssen für die Operatoren eine eigene Funktion definieren werden. Wie dies geht, wird im Kapitel Allgemeines Überladen von Operatoren beschrieben.
Erwähnt werden soll noch, dass der enum-Datentyp und die Enumeratoren mit Attributen versehen werden können. Mehr zu den Attributen im Kapitel Präprozessor-Direktiven und Attribute.