C++11 Enums

von Hubert Schmid vom 2011-10-09

Mit der Version C++11 finden einige größere und viele kleinere Verbesserungen Einzug in die Sprache C++. In diesem Artikel werfe ich einen ausführlicheren Blick auf die Änderungen rund um die Enumerations.

Einleitung

Die erste Version von C++ übernahm die enums noch weitgehend unverändert aus C. Im Gegensatz zu vielen anderen Sprachkonstrukten wurden die Enumerations damals nicht an die Konzepte von C++ angepasst. Der neue Sprachstandard holt das nun nach. Im Fokus stehen dabei keine grundlegenden Änderungen, sondern vor allem eine bessere Integration in die Sprache, die den Entwicklern die tägliche Arbeit vereinfacht und die Lesbarkeit und Verständlichkeit der Programme verbessert.

Im folgenden betrachte ich drei Änderungen genauer.

Syntaktische Änderungen

Die Sprachentwickler waren sehr bemüht, den neuen Sprachstandard abwärtskompatibel zu halten. Das bedeutet im Wesentlichen, dass alle bisherigen, wohldefinierten Programme auch weiterhin übersetzbar sind und sich identisch verhalten. Dieses Vorgehen finde ich sehr lobenswert und nicht selbstverständlich. Es führt aber leider dazu, dass man der Sprache diese inkrementelle Entwicklung und das Alter ansieht.

Für Enumerations bedeutet das, dass es nun zwei Varianten gibt, und dass die neue Variante nicht nur auf den ersten Blick ein wenig merkwürdig aussieht. Während die abwärtskompatiblen Aufzählungen auch weiterhin mit dem Schlüsselwort enum eingeleitet werden, verwenden die neuen Enumerations die Kombination enum class.

Bei den bisherigen Enumerations befinden sich die definierten Werte im gleichen Namensraum wie der Typ selbst. Das ist im folgenden Beispiel sichtbar:

namespace ex { enum color { yellow, orange, red }; } // NOTE: What is ex::orange? ex::color background = ex::orange;

Das kann schnell zu Namenskonflikten führen. Und außerdem ist der Zusammenhang zum Typ nicht direkt ersichtlich. Daher geben viele Programmierrichtlinien für C und C++ vor, dass die Werte mit einem entsprechenden Präfix versehen werden. Das vorherige Beispiel würde in der Praxis daher eher so aussehen:

namespace ex { // coding conventions require prefix color_ for all values. enum color { color_yellow, color_orange, color_red }; } ex::color background = ex::color_orange;

Die neuen Enumerations beseitigen diese Altlast. Die definierten Werte leben nun immer in ihrem eigenen Namensraum. Die Programmierrichtlinie wird überflüssig und der Bezug zum Typ automatisch sichergestellt. Auch wenn man dadurch in Summe nicht viel Arbeit beim Schreiben spart, so fühlt sich die neue Sichtbarkeitsregelung zumindest viel mehr nach C++ an.

namespace ex { // new enum type enum class color { yellow, orange, red }; } ex::color background = ex::color::orange;

Starke Typisierung

In C und C++ können viele Datentypen implizit in einen Integer- oder Boolean-Datentypen konvertiert werden. Das gilt auch für die bisherige Variante der Enumerations. Intuitiv sind diese impliziten Typkonversionen in vielen Fällen nicht, was schnell zu Fehlern führen kann.

Im folgenden Beispiel erfolgt eine solche implizite Typumwandlung nach bool innerhalb der if-Bedingung. Ohne einen zusätzlichen Kommentar ist unklar, ob die implizite Konvertierung wirklich beabsichtigt ist. Und selbst wenn dem Autor das Verhalten an dieser Stelle klar war, so ist zumindest äußert fragwürdig, ob ein anderer Entwickler die Bedingung beim Lesen ebenfalls unmittelbar richtig versteht.

enum trivalent { yes, no, error }; trivalent file_exists(const char* pathname); void do_something(const char* pathname) { if (file_exists(pathname)) { // ... } }

Bei den neuen Enumerations gibt es diese implizite Typumwandlung nicht mehr – vermutlich hauptsächlich aufgrund der oben beschriebenen Fehleranfälligkeit. Es gibt aber durchaus Fälle, in denen die Integer-Repräsentationen hilfreich sind – beispielsweise innerhalb einer Debug-Ausgabe oder bei der Serialisierung der Daten. Um das mit den neuen Datentypen zu erreichen, ist nun eine explizite Typumwandlung notwendig.

enum class color { yellow, orange, red }; int color_to_int(color c) { return static_cast<int>(c); }

Der Code wird dadurch ein wenig länger. Es ist aber anzunehmen, dass diese Funktionalität nur selten benötigt wird. Und der zusätzliche Cast verbessert die Lesbarkeit, da die Intention des Entwicklers deutlich wird.

Das letzte Beispiel enthält allerdings ein neues Problem. Es ist unklar, ob der Datentyp int überhaupt passend ist. Diesem Thema widmet sich der folgende Teil.

Unterliegender Datentyp

Im Gegensatz zu vielen anderen Sprachen können in C und C++ die Enum-Datentypen nicht nur die explizit definierten Werte annehmen, sondern alle Werte des darunter liegenden Integer-Datentyps. Allerdings war der genaue Datentyp bisher nicht eindeutig ermittelbar – oder zumindest von der Implementierung abhängig. Und man musste ein paar Tricks anwenden, um diese Problematik zu umgehen. So wurde beispielsweise ein großer Wert im Enum definiert, um eine Mindestgröße zu erzwingen. Und umgekehrt wurde beispielsweise ein Enum mit dem unären +-Operator in einen Integer-Datentyp hinreichender Größe konvertiert.

Die neue C++‑Version bietet nun Möglichkeiten für beide Richtungen und für beide Varianten der Enums. Der unterliegende Integer-Datentyp kann bei der Deklaration optional angegeben werden. Und mit dem Template std::underlying_type kann der Typ wiederum bestimmt werden.

// specifying the underlying type for new enums enum class fruit : uint16_t { apple, pear, orange }; // ... and for old enums. enum color : uint32_t { yellow, orange, red };

Im Gegensatz zu den meisten anderen Features ist das Template std::underlying_type in GCC‑4.6 leider noch nicht verfügbar. Das folgende Beispiel konnte ich daher nicht testen, versucht aber dennoch zu zeigen, wie diese Funktionalität verwendet werden könnte.

enum class color : uint32_t { yellow, orange, red }; template <typename EnumType> void write(std::ostream& os, EnumType value) { typedef std::underlying_type<EnumType> ul; os << static_cast<typename ul::type>(value); } int main() { write(std::cout, color::orange); }

Fazit

In diesem Artikel habe ich die wichtigsten Erweiterungen der Enum-Datentypen vorgestellt, die mit der neuen Sprachversion in C++ einziehen. Auch wenn die Änderungen gering erscheinen, so bringen sie doch viele Vorteile für den Entwickler mit sich, und machen den Einsatz von Enums in C++ wieder attraktiver. Das Laufzeitverhalten hat sich dagegen nicht verändert. Und das macht es umso einfacher, auf die neuen Erweiterungen sicher und schnell umzusteigen.