1. Einführung
Der gestrige Datensatz war sehr bequem:
struct Task {
int id; // Aufgaben‑id
std::string title; // Aufgabenname
bool done = false; // Status: erledigt/nicht erledigt
};
Das Problem beginnt an dem Tag, an dem Sie einen dritten Status hinzufügen möchten: „in Bearbeitung“.
Und hier versagt bool, weil er nur „ja/nein“ kann, während wir „drei verschiedene Bedeutungen“ wollen.
Der häufigste „Quick Fix“ von Anfängern ist, ein zweites Flag hinzuzufügen:
struct Task {
int id;
std::string title;
bool done = false;
bool inProgress = false; // können beide true sein? oder beide false?
};
Der Code kompiliert. Die Logik — nicht mehr. Jetzt sind Zustände möglich:
unddone = true
(Aufgabe gleichzeitig erledigt und in Bearbeitung… Quantenmechanik?),inProgress = true
unddone = false
(scheinbar „todo“, aber das ist eine Vereinbarung „zwischen den Zeilen“).inProgress = false
Je mehr Status — desto mehr Flags und desto mehr „illegale Kombinationen“.
„Wir machen int status“ — magische Zahlen betreten die Bühne
Nächste Idee: „Ich kodiere es einfach“.
int status = 0; // 0=todo, 1=in_progress, 2=done (im Kopf)
status = 42; // der Compiler hat nichts dagegen, aber die Logik weint
Der Nachteil liegt nicht im Typ int. Der Nachteil ist, dass der Typ die Bedeutung nicht ausdrückt. Wenn Sie in einer Woche if (status == 2) sehen, ist das kein Code — das ist ein Rätsel.
„Wir machen std::string status“ — Tippfehler werden zum neuen Status
Wir wechselten zu Strings, aber es wurde nicht besser:
std::string status = "done";
status = "inprogres"; // Tippfehler -> „neuer Status“, über den sich niemand geeinigt hat
Strings führen oft zu einem Variantenzoo: "Done", "done", "DONE", "in progress", "in_progress"…
Was wir wirklich brauchen
Wir brauchen einen Typ, der sagt: der Wert kann nur einer von vorher bekannten Varianten sein. Das sind Aufzählungen: enum und enum class. enum ist die ältere Variante und wird selten noch verwendet. Daher schauen wir uns direkt das moderne enum class an.
Kleine „Ergebnis‑Tabelle“:
| Wie den Status speichern | Beispiel | Problem |
|---|---|---|
|
|
nur 2 Zustände, danach fangen Hacks an |
|
|
magische Zahlen, leicht „versehentlich 42“ |
|
|
Tippfehler erzeugen neue „Werte“ |
|
|
type‑safe und selbstdokumentierend |
2. enum class: Basissyntax und Arbeit mit Werten
Deklaration
Mit enum class kann man einen neuen Typ erstellen, indem man alle möglichen Werte aufzählt:
enum class Status {
Todo,
InProgress,
Done
}; // ja, das Semikolon ist nötig
Hier:
- Status — Typname
- Todo, InProgress, Done — Aufzählungswerte (mögliche Werte)
- ; nach } ist verpflichtend (wie bei struct)
Erstellen, zuweisen, vergleichen
Eine Aufzählung ist ein „normaler“ Typ: man kann sie in einer Variablen speichern, zuweisen und vergleichen.
enum class Status { Todo, InProgress, Done };
void Demo() {
Status s = Status::Todo; // erstellt
s = Status::InProgress; // zugewiesen
if (s == Status::InProgress) { // verglichen
// etwas tun
}
}
Gewohnheit, die man sich merken sollte: bei enum class schreibt man die Werte als Status::Todo, nicht nur Todo. Das ist kein unnötiger Lärm, sondern Namensschutz gegen Konflikte.
Wichtiger Hinweis: enum class wird nicht automatisch zu int oder bool
Der alte Typ enum konnte sich automatisch in int verwandeln und sogar als „wahr/false“ benutzt werden.
enum Status { Todo, InProgress, Done };
void Bad() {
Status s = Done;
int x = s;
if (s) { }
}
Sehr bequem und sehr… problematisch. Im neuen enum class wurde das abgeschafft. Der Compiler lässt nicht mehr zu, dass „Status“ versehentlich als Zahl oder Bool genutzt wird.
3. Wir fügen enum class in unser Modell Task ein
Wir schreiben Task um: statt bool done verwenden wir einen echten Status.
#include <string>
enum class Status { Todo, InProgress, Done };
struct Task {
int id;
std::string title;
Status status = Status::Todo; // Standardstatus
};
Jetzt werden Aufgaben deutlich „sprechender“ erzeugt:
Task a{1, "Buy milk"}; // status = Todo (Standard)
Task b{2, "Pay rent", Status::InProgress}; // explizit gesetzt
Und eine nette Kleinigkeit: es ist unmöglich, aus Versehen etwas Unpassendes zuzuweisen.
Task t{1, "Fix bug"};
// t.status = 2; // Kompilierungsfehler: genau das, was wir wollen
4. Status und Prioritäten: noch ein enum class
Da wir gelernt haben, wie man „abgeschlossene Wertemengen“ ausdrückt, fügen wir eine weitere Menge hinzu: Priorität.
enum class Priority {
Low,
Normal,
High
};
Und erweitern das Modell Task. Gute Nachricht: wir können das Feld so hinzufügen, dass alter Code kaum bricht — einfach einen Standardwert vergeben.
#include <string>
enum class Status { Todo, InProgress, Done };
enum class Priority { Low, Normal, High };
struct Task {
int id;
std::string title;
Status status = Status::Todo;
Priority priority = Priority::Normal;
};
Nun können wir schreiben:
Task a{1, "Buy milk"}; // Todo + Normal
Task b{2, "Pay rent", Status::InProgress}; // InProgress + Normal
Task c{3, "Fix prod", Status::Todo, Priority::High}; // Todo + High
Und hier wird Type‑Safety fast „greifbar“:
Task t{1, "Write report"};
t.status = Priority::High; // Fehler: unterschiedliche Typen!
t.priority = Status::Done; // ebenfalls Fehler
Wären beide Felder int, wären solche Fehler real und sehr unangenehm.
5. Was ist besser: enum vs enum class
In C++ gibt es zwei Arten von Aufzählungen:
- enum — unscoped (alter Stil)
- enum class — scoped (moderner Stil, meist bevorzugt)
Problem des normalen enum: Namen „laufen aus“
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // Namenskonflikt Red/Green
In großen Projekten wird das zu konstanten „Warum ist Red schon belegt?“-Konflikten.
Problem des normalen enum: zu leicht in int zu werden
enum Color { Red, Green, Blue };
void Surprise() {
int x = Red; // kompiliert
if (Green == 1) { // kompiliert (fragwürdig)
// ...
}
}
Das heißt, enum kann in Zahlen „auslaufen“ und Sie sind wieder bei magischen Werten.
Was enum class bietet
Vorteile von enum class:
- hält Namen innerhalb des Typs (Beispiel: Status::Done)
- erlaubt keine implizite Konversion in Zahl/Bool
Kleine Merkhilfe:
| Eigenschaft | |
|
|---|---|---|
| Namen der Werte | im aktuellen Sichtbereich | innerhalb des Typs () |
Implizite Konversion zu |
oft ja | nein |
| Weniger Namenskonflikte | nein | ja |
| „Standard“ in modernem C++ | eher nein | ja |
Praktische Regel des Kurses: wenn Sie unsicher sind — nehmen Sie enum class.
6. Numerische Werte der Aufzählungswerte
„Standard“-Werte
Wenn Sie die Zahlen nicht explizit angeben, laufen die Werte normalerweise als 0, 1, 2 … (in Reihenfolge).
enum class Status { Todo, InProgress, Done };
// normalerweise Todo=0, InProgress=1, Done=2
Wichtig: das ist eine Darstellungs‑Detail, nicht die Bedeutung. Die Bedeutung schreibt man so:
if (t.status == Status::Done) {
// Aufgabe ist fertig
}
und nicht so:
if (code == 2) { ... } // das bringt uns zurück zu magischen Zahlen
Explizite Codes: wann das wirklich nötig ist
Manchmal benötigt man stabile Zahlen (z. B. Dateiformat/Protokoll/Integration mit fremder API). Dann setzt man Werte explizit:
enum class Status {
Todo = 10,
InProgress = 20,
Done = 30
};
Wenn Sie einem Element einen Wert geben, zählt das nächste ohne Wert weiter:
enum class Status {
Todo = 10,
InProgress, // 11
Done // 12
};
Das ist praktisch, verlangt aber Disziplin: wenn Sie Zahlen einführen — dokumentieren Sie, warum.
7. Underlying type: welcher Typ wird tatsächlich im enum gespeichert
Die Aufzählung wird im Speicher als ganze Zahl abgelegt. Der Typ dieser Zahl heißt underlying type (Basis‑Typ des enum). Meistens ist das etwas wie int, aber manchmal möchte man den Typ explizit angeben.
Häufigste Gründe:
- Kompatibilität mit externem Format (z. B. „im File wird der Status als 1 Byte gespeichert“)
- Speicherersparnis in großen Arrays/Tabellen
- klaren Bereich begrenzen wollen
Syntax: : std::uint8_t
So ersetzt man das interne int durch std::uint8_t:
#include <cstdint>
enum class Status : std::uint8_t {
Todo,
InProgress,
Done
};
Hier gibt es zwei praktische Hinweise.
Erstens: alle Werte müssen in den gewählten Typ passen. Zweitens: auch wenn Status 1 Byte groß ist, kann sizeof(Task) wegen Alignment (Padding) nicht kleiner werden. Das ist kein Bug, das ist normale „Speichergeometrie“.
Wenn Sie experimentieren möchten (und zugleich sehen wollen, dass der Compiler ein lebender Organismus ist, kein Taschenrechner), können Sie Größen ausgeben:
#include <iostream>
int main() {
std::cout << sizeof(Status) << '\n';
std::cout << sizeof(Task) << '\n';
}
Die Ergebnisse können zwischen Compilern/Plattformen variieren — das ist normal.
8. Wie erhält man den numerischen Wert eines enum
std::to_underlying — der korrekte „offizielle“ Weg
In C++23 gibt es die Funktion std::to_underlying, die den underlying‑Wert der Aufzählung zurückgibt.
#include <iostream>
#include <utility> // std::to_underlying
enum class Status { Todo, InProgress, Done }; // 0, 1, 2
int main() {
Status s = Status::Done;
std::cout << std::to_underlying(s) << '\n'; // 2
}
Das ist nützlich zum Debugging oder zum „Durchreichen“ in ein Protokoll/Datei (wenn das so vereinbart ist).
Falle: std::uint8_t in cout kann als Zeichen ausgegeben werden
Wenn der underlying type std::uint8_t ist, sieht man beim Stream‑Ausgabe eventuell ein „Zeichen“ statt einer Zahl. Denn uint8_t verhält sich oft wie unsigned char.
#include <cstdint>
#include <iostream>
#include <utility>
enum class Status : std::uint8_t { Todo, InProgress, Done };
int main() {
auto code = std::to_underlying(Status::Done);
std::cout << static_cast<int>(code) << '\n'; // als Zahl ausgeben
}
Regel: Wenn das underlying „byte“ ist, casten Sie zum Ausgeben auf int.
Wenn std::to_underlying nicht verfügbar ist: static_cast
Manchmal steckt die Umgebung in einem älteren Standard (oder C++23 ist nicht aktiviert). Dann hilft das explizite Casting:
enum class Status { Todo, InProgress, Done };
int code = static_cast<int>(Status::Done); // 2
Der Sinn ist derselbe: Sie sagen dem Compiler ausdrücklich „ja, ich will die Zahl“.
9. Gefährliche Zone: Konversion von einer Zahl in ein enum
Tatsächlich ist static_cast<Status>(42) eine potenzielle Zeitbombe. So ein Code kompiliert:
enum class Status { Todo, InProgress, Done };
Status s = static_cast<Status>(42); // formal ok, logisch — Müll
Sie haben gerade einen Status erzeugt, der im Aufzählungstyp kein Namen hat. Das bricht die Idee der „abgeschlossenen Wertemenge“.
enum class schützt vor Zufallsfehlern, aber wenn Sie „mit dem Vorschlaghammer“ durch static_cast die Tür aufstoßen, ist der Compiler nicht verpflichtet, Sie zu retten.
Feinheit bei Status s{};
Value‑Initialisierung ({}) ergibt ein underlying‑Null‑Wert.
enum class Status { Todo, InProgress, Done };
Status ok{}; // underlying=0 -> das ist Todo
Aber wenn Sie Codes nicht bei Null beginnen:
enum class Status { Todo = 10, InProgress = 20, Done = 30 };
Status bad{}; // underlying=0 -> das ist NICHT Todo, das ist ein „unnamed“ Wert
Praktischer Schluss: Wenn Sie einen Default‑Status wollen, geben Sie ihn explizit an. In unserem Modell machen wir das so:
Status status = Status::Todo;
Wenn Sie ein enum aus einer Zahl erhalten müssen — validieren Sie
Wir behandeln noch nicht std::optional und „korrekte Parser‑Ergebnisse“ (das kommt später), aber das Prinzip lässt sich einfach zeigen: prüfen Sie die Zahl vor der Zuweisung.
enum class Status { Todo, InProgress, Done };
bool TryParseStatus(int code, Status& out) {
if (code == 0) { out = Status::Todo; return true; }
if (code == 1) { out = Status::InProgress; return true; }
if (code == 2) { out = Status::Done; return true; }
return false; // unbekannter Code
}
Ja, hier tauchen wieder Zahlen auf — aber jetzt leben sie an einer Stelle und werden nur als Eingabeformat benutzt, nicht als Grundlage der Logik im ganzen Programm.
10. using enum: weniger Tippen für Status::...
C++20 hat using enum eingeführt, mit dem man die Enumerationswerte in den aktuellen Sichtbereich „holen“ kann.
enum class Status { Todo, InProgress, Done };
void Demo() {
using enum Status; // Todo/InProgress/Done sind ohne Status:: sichtbar
Status s = Todo;
s = InProgress;
}
Das ist praktisch, aber gute Praxis lautet: nutzen Sie using enum lokal, innerhalb einer Funktion. Wenn Sie es global machen, erzeugen Sie leicht Konflikte und fragen sich „was ist Todo und woher kommt es?“.
11. Typische Fehler beim Arbeiten mit enum / enum class
Fehler Nr. 1: Semikolon ; nach der Aufzählung vergessen.
Symptome sehen oft aus wie „expected ‘;’ …“ oder seltsame Fehler in den folgenden Zeilen. Heilung: enum class {...}; — immer mit Semikolon.
Fehler Nr. 2: schreiben Todo statt Status::Todo.
Bei enum class liegen die Werte im Typ. Das ist extra so, um Konflikte zu vermeiden und den Code eindeutig lesbar zu machen. Wenn Sie es kürzer wollen, nutzen Sie using enum Status; lokal in einer Funktion.
Fehler Nr. 3: versuchen, enum class als int oder bool zu verwenden.
enum class konvertiert sich nicht automatisch in Zahlen oder logische Werte. Das ist kein Sprachlauneflick — es schützt vor sinnlosen Prüfungen wie if (status) oder Zuweisungen wie status = 2;.
Fehler Nr. 4: Geschäftslogik auf Zahlen 0/1/2 aufbauen.
Wenn Sie irgendwo if (code == 2) sehen, hat das enum seine Aufgabe nicht erfüllt, und Sie sind wieder bei magischen Zahlen. Richtiger Code vergleicht mit Status::Done, nicht mit 2.
Fehler Nr. 5: static_cast<Status>(Zahl) ohne Prüfung.
So kann man einen „unnamed“ Status erhalten, der weder Todo noch InProgress noch Done ist. Danach verhält sich das Programm seltsam, weil Sie selbst den Typenschutz umgangen haben.
GO TO FULL VERSION