C++: Die Basics - eps1.6_Enumeration
Hallo und herzlich willkommen. Ich weiß, es ist schon ein paar Tage her und im heutigen Informationszeitalter ist das eine halbe Ewigkeit, doch wir haben uns mit Typisierung beschäftigt. Dort haben wir die primitiven Datentypen von C++ kennengelernt.
Mit ihrer Hilfe kannst du schon sehr gut Wertebereiche einschränken und für Typensicherheit sorgen. Benötigst du allerdings komplexere Datentypen oder sind diese für deinen Anwendungsfall zu eindeutig, steht es dir frei zu sagen:
„Das reicht uns nicht!“
Glücklicherweise lässt dich C++ aus den Grunddatentypen eigene zusammenstellen und definieren.
Enumerations - Aufzählungstyp
Es gibt Situationen, in denen eine Variable nur bestimmte Werte akzeptieren soll und vielleicht die Werte sogar keinem numerischen Typen entsprechen. In diesem Fall sind Enumerationen dein Werkzeug der Wahl.
Beginnend mit Keyword enum
kannst du einen Typnamen vergeben und eine Auflistung der Werte deines benutzerdefinierten Datentyps erstellen. Die einzelnen Elemente deiner Auflistung heißen Enumeratoren. Anschließend kannst du direkt Variablen deines neuen Datentyps deklarieren. Wichtig zu beachten ist, dass die Definition deines neuen Datentyps mit einem Semikolon ;
abgeschlossen wird. Egal, ob du jetzt schon Variable deklarierst oder nicht.
// listing1: enums syntax
enum class TypName
{
ListOfValues;
} ListOfVariables;
Soviel zur Syntax von Enumerationen. Die Aufzählungstypen haben ihre Bezeichnung, da sie intern ihre Werte auf die natürlichen Zahlen abbilden. Deine aufgelisteten Werte werden mit 0 beginnend durchnummeriert.
Mit einem Beispiel wird die Funktionsweise sicherlich klarer. Unser neuer Datentyp cardinalDirection
stellt die vier Himmelsrichtungen dar. Nach der Definition der Enumeration wird der Variablen myDirection
der Wert East
zugewiesen.
// listing2: cardinalDirection enumeration
enum class cardinalDirection
{
North, // = 0
East, // = 1
South, // = 2
West // = 3
} myDirection;
int main()
{
myDirection = cardinalDirection::East;
return 0;
}
Was geschieht in listing2? Eingeleitet durch das Schlüsselwort enum
mit dem Zusatz class
wird der Datentyp cardinalDirection
global definiert. Innerhalb der geschweiften Klammern sind die Werte North
, East
, South
und West
als die einzigen gültigen Werte dieses Typs aufgelistet und jeweils durch ein Komma getrennt. Nach der abschließenden Klammer wird direkt die erste Variable myDirection
unseres Datentyps deklariert. Das folgende Semikolon zeigt das Ende der Definition an.
Darauf folgend wird in der main()
Funktion East
als Wert der Variablen zugewiesen. Nur einer der vier Werte aus unserer Definition wird als gültig akzeptiert. Wie du siehst, verlangt die Syntax keine Sonderzeichen. Das geht, da der Datentyp cardinalDirection
ausdrücklich den Wert East
akzeptiert.
Intern sieht das allerdings ganz anders aus. Der Compiler konvertiert die Enumeratoren zu Integerwerten. Das bedeutet, dass jedem in der Enumration aufgeführten Wert eine Zahl zugeordnet wird. In unserem Beispiel bekommt North
die 0
, East
die 1
, South
die 2
und West
die 3
. Das ist der Standardfall. Wenn du möchtest, dann kannst du bei der Definition für jeden Wert eine beliebige Zahl zuweisen.
// listing3: enumeration with custom interger values
enum class cardinalDirection
{
North = 78,
East = 6,
South = 0,
West = -1
} myDirection;
Wozu dieses „class”?
Häufig wirst du im Quellcode von anderen die Enumeration Deklaration ohne den Zusatz class
finden. Das Schlüsselwort wurde 2011 in C++ eingeführt. Du kannst es auch weglassen und hast weiterhin gültiges C++. Doch die Verwendung ist durchaus sinnvoll. class
stärkt die Typsicherheit. Dadurch werden implizite Umwandlungen der Enumeratonen nach int
verhindert.
// listing4: implicit converting to int
// enum
enum colorRGB { red, green, blue };
int i = red + blue; // equivalent to 0 + 2
// enum class
enum class colorCMYK {cyan, magenta, yellow, key};
int j = colorCMYK::cyan + colorCMYK::magenta; // error
Neben der impliziten Umwandlung von red
und blue
zu 0
uns 2
fällt auf, dass die Typqualifizierung color::
fehlen kann. Genau unterscheiden sich die beiden Varianten darin, dass in enum class
die Enumeratoren lokal in der Enumeration existieren. Deshalb kann keine implizite Konvertierung in andere Typen stattfinden. Bei einfachem enum
befinden sich die Enumeratoren im selben Geltungsbereich, wie die Enumeration und die implizierte Konvertierung funktioniert.
Aufgrund der erhöhten Typsicherheit solltest du aber enum class
verwenden. Natürlich steht es dir frei, in der jeweiligen Situation selbst zu entscheiden und in begründeten Ausnahmefällen darauf zu verzichten.
enum class
entfernt die Möglichkeit der Konvertierung natürlich nicht, sondern verlangt von dir, diese Anweisung bewusst zu erteilen.
Der direkte Vergleich
// listing5: comparison enum and enum class
#include <iostream>
int main()
{
// enum
enum colorRGB
{
red,
green,
blue
} colorAdd;
colorAdd = red; // assign
int i = colorAdd; // implicit convertion
colorAdd = (colorRGB)2; // old style casting
std::cout << "Converted Interger: " << i << " Convereted Enum: " << colorAdd << std::endl; // cout of the enum, impicit convertion
// enum class
enum class colorCMYK
{
cyan,
magenta,
yellow,
key
} colorSub;
colorSub = colorCMYK::key; // assign
int j = static_cast<int>(colorSub); // casting to int
colorSub = static_cast<colorCMYK>(1); // casting to enum class
std::cout << "Converted Interger: " << j << " Convereted Enum: " << static_cast<int>(colorSub) << std::endl; // cout of the enum class
return 0;
}
Da Enumeratoren intern als Interger abgebildet werden, kannst du auch jede beliebige Zahl der Enumartion zuweisen. Achte dabei aber darauf, dass für die Zahl ein entsprechender Enumerator existiert.
Es spielt also keine Rolle, ob du eine Zahl oder einen gültigen Wert der Variablen zuweist. Das Ergebnis ist das gleiche. Du musst jedoch die Zahl explizit mit einem cast
in einen Wert vom Typ der Enumeration umwandeln.
Typenumwandlung
„Was ist
static_cast
und was bedeutet diesescast
von dem er da spricht?“
Meist sind Typumwandlungen ein Zeichen für kein gut durchdachtes Programmdesign. Deshalb solltest du dir lieber nochmal darüber Gedanken machen. Aber es gibt auch Anwendungsfälle, in denen Konvertierungen notwendig sind.
Ich möchte an dieser Stelle auch nicht zu tief in das Thema Casting einsteigen. Doch jetzt habe ich in unseren Beispielen zu Enumerationen Castings benutzt. Deshalb gibt es einen kurzen Überblick.
Wenn wir eine Variable deklarieren, legen wir dabei ihren Datentyp fest. Der Typ einer Variablen kann anschließend nicht mehr verändert werden. Ergibt sich eine Konstellation, in der du gerne den Wert einer Variablen weiterverwenden möchtest, aber der Datentyp nicht passt, dann kannst du den Typ in den passenden konvertieren. Diese Typenumwandlung wird Casting genannt.
Implizit und Explizit
Der Ursprung von C++ liegt in der Programmiersprache C und Compiler versuchen, rückwärts kompatibel zu bleiben, um älteren Code weiterhin bauen zu können. In C gibt es die beiden Varianten der implizierten und explizierten Castings.
Bei einem impilizierten cast versucht der Compiler Annahmen über die Absichten des Programmierers zu treffen, anstatt die Datentypenfehler zu schmeißen. Er konvertiert die Typen automatisch.
// listing6: implicit casting
#include <iostream>
int main()
{
float f = 1.5;
int i = f; // implicit cast
std::cout << "Float " << f << " converts to Int " << i << std::endl;
return 0;
}
Das implizierte Casting funktioniert nicht immer und liegt in der Entscheidungsgewalt des Compilers. Um mehr Kontrolle darüber zu haben, wann und wie der Typ umgewandelt wird, kannst du auch explizit die Anweisung dazu geben. Dazu musst du nur den gewünschten Datentypen in Klammern vor die entsprechende Variable setzen (siehe listing 7).
// listing7: explicit casting
#include <iostream>
int main()
{
int dividend = 3;
int divisor = 4;
float quotient = dividend/divisor; // result = 0
std::cout << " Without explicit cast: " << quotient << std::endl;
float quotientCast = (float)dividend/divisor; // result 0.75
std::cout << " With explicit cast: " << quotientCast << std::endl;
return 0;
}
C ist nicht sehr streng, wenn es um Castings geht. Gibst du explizit die Anweisung, dann wird ohne zu prüfen umgewandelt. Und das sogar bei Pointern. Aus diesem Grund sind C-Style Typenumwandlungen nicht ganz ungefährlich und werden von C++-Programmierern nicht empfohlen.
Die C++ Casting Operatoren
Auch wenn Typenumwandlungen die Typensicherheit stark bedrohen, lassen sie sich trotzdem nicht einfach verwerfen. In einigen Situationen sind sie hilfreich und lösen Kompatibilitätsprobleme. Darum führt C++ vier neue Casting Operatoren ein, die für mehr Kontrolle und Sicherheit sorgen:
- static_cast
- dynamic_cast
- reinterpret_cast
- const_cast
Dass es nun vier spezialisierte Operatoren und nicht mehr nur einen Cast für alles gibt, vermeidet unerwünschte Effekte. Zudem sind diese Castings wegen ihrer Syntax optisch leicht zu erkennen und machen den Einsatz dem Programmierer bewusster.
// listing8: syntax of the casting operators
destination_type result = cast_operator<destination_type> (object_to_cast);
Ich möchte hier nicht auf alle im Detail eingehen. Aber ich möchte dir static_cast
vorstellen. In Listing5 habe ich den Operator dazu genutzt, den Enumerator unseres eigenen Datentyps in einen Integer zur Ausgabe mit std::cout
konvertiert.
static_cast
ist für die Umwandlung von Standard Datentypen gedacht und sollte deine erste Wahl in den meisten Fällen sein. Der Operator erfüllt die Aufgabe des implizierten Castings und bringt dir den größten Vorteil bei der explizierten Umwandlung von Pointern. Sprich, wir driften in ein fortgeschrittenes Thema ab.
Merke dir einfach als Fazit: Um die Typensicherheit deines Quellcodes nicht mehr als nötig zu gefährden, verwende static_cast
und nicht die alten Varianten. Behalte deine Autorität und gib die Verantwortung nicht an den Compiler ab.
Das bleibt hängen
Unser erster eigener Datentyp ist eine Enumeration. Wir haben die gültigen Werte und den Wertebereich selbst festgelegt, sogar die ersten Variablen definiert. Intern werden unsere Werte auf Interger abgebildet und fortlaufend nummeriert. Allerdings können wir die Numerierung auch selbst bestimmen.
Wir haben uns angeschaut, wie wir die Typsicherheit für unsere Datentypen mit enum class
erhöhen und mit static_cast
in einen anderen Typen konvertieren.
Eine Variable unseres Enumeration-Datentyps kann nicht nur lokal oder global definiert werden. Du kannst sie ebenso innerhalb einer Struktur einsetzen. Diese lernen wir nächstes Mal kennen.
Bis dahin wünsche ich dir maximalen Erfolg!
Quellen
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017