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 dieses cast 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