C++ Core Guidelines: Regeln für Aufzählungen

Modernes C++  –  0 Kommentare

Das Kapitel zu Aufzählungen besitzt acht Regeln. Seit C++11 kennt C++ Aufzählungen mit eigenem Gültigkeitsbereich (scoped enums), die die Nachteile der klassischen Aufzählungen überwinden.

Aufzählungen stellen eine Menge von Ganzzahlen dar, die sich wie ein Datentyp verhalten. Hier ist der Überblick zu den acht Regeln.

Wie im Vorspann genannt: Klassische Aufzählungen besitzen viele Nachteile. Daher will ich gerne explizit klassische Aufzählungen ohne eigenen Gültigkeitsbereich (unscoped enums) und Aufzählungen mit eigenem Gültigkeitsbereich, die gerne auch streng typisierte Aufzählungen genannt werden, vergleichen. In den C++ Core Guidelines fehlt mir dieser wichtige Vergleich.

Hier ist eine klassische Aufzählung:

enum Colour{
red,
blue,
green
};

Welche Nachteile besitzen sie?

  • Die Aufzähler besitzen keinen eigenen Gültigkeitsbereich
  • Die Aufzähler konvertieren heimlich zu int
  • Die Aufzähler verschmutzen den globalen Namensbereich
  • Der Typ der Aufzähler ist nicht definiert. Er muss nur groß genug sein, um die Werte der Aufzähler darzustellen.

Durch das Verwenden des Schlüsselwortes class oder struct wird die klassische Aufzählung zu einer Aufzählung mit Gültigkeitsbereich (scoped enum oder auch enum class).

enum class ColourScoped{
red,
blue,
green
};

Nun ist der Bereichsoperator :: notwendig, um auf die Aufzähler zuzugreifen: Colour::red. Colour::red konvertiert nicht heimlich zu int und verschmutzt daher auch nicht den globalen Namensbereich. Zusätzlich ist der Typ der Aufzähler per Default int.

Nach dieser Hintergrundinformation geht es direkt zu den Regeln.

Enum.1: Prefer enumerations over macros

Makros ignorieren Gültigkeitsbereich und besitzen keinen Datentyp. Das bedeutet, du kannst ein bereits gesetztes Makro, dass für eine Farbe steht, überschreiben.

// webcolors.h 
#define RED 0xFF0000

// productinfo.h
#define RED 0

int webcolor = RED; // should be 0xFF0000

Mit der Aufzähler ColourScoped wird dieser Fehler nicht auftreten, denn er benötigt seinen eigenen Gültigkeitsbereich: ColourScoped webcolour = ColourScoped::red;

Enum.2: Use enumerations to represent sets of related named constants

Diese Regel ist leicht einsichtig, denn Aufzählungen sind Mengen von Ganzzahlen, die sich ein Datentyp verhalten.

Enum.3: Prefer enum classes over “plain” enums

Die Aufzähler einer Aufzählung mit Gültigkeitsbereich konvertieren nicht heimlich zu int. Sie müssen gegebenenfalls explizit konvertiert werden. Um auf sie zuzugreifen, ist der Bereichsoperator notwendig.

// scopedEnum.cpp

#include <iostream>

enum class ColourScoped{
red,
blue,
green
};

void useMe(ColourScoped color){

switch(color){
case ColourScoped::red:
std::cout << "ColourScoped::red" << std::endl;
break;
case ColourScoped::blue:
std::cout << "ColourScoped::blue" << std::endl;
break;
case ColourScoped::green:
std::cout << "ColourScoped::green" << std::endl;
break;
}
}

int main(){

std::cout << static_cast<int>(ColourScoped::red) << std::endl; // 0
std::cout << static_cast<int>(ColourScoped::red) << std::endl; // 0

std::cout << std::endl;

ColourScoped colour{ColourScoped::red};
useMe(colour); // ColourScoped::red

}

Enum.4: Define operations on enumerations for safe and simple use

Die Guidelines definiert zum Verdeutlichen ihrer Regel eine Aufzählung Day. Diese Aufzählung unterstützt den Inkrement-Operator.

enum Day { mon, tue, wed, thu, fri, sat, sun };

Day& operator++(Day& d)
{
return d =
(d == Day::sun) ? Day::mon : static_cast<Day>(static_cast<int>(d)+1);
}
Day today = Day::sat;
Day tomorrow = ++today;

Der static_cast in dem Beispiel ist notwendig. Falls der Inkrement-Operator im Inkrement-Operator zum Einsatz käme, wäre die Folge eine unendliche Rekursion.

Day& operator++(Day& d)
{
return d = (d == Day::sun) ? Day::mon : Day{++d}; // error
}

Enum.5: Don’t use ALL_CAPS for enumerators

Falls du ALL_CAPS für die Aufzähler verwendest, können Konflikte mit Makros auftreten, da diese typischerweise in Großbuchstaben geschrieben werden.

#define RED 0xFF0000

enum class ColourScoped{ RED }; // error

Enum.6: Avoid unnamed enumerations

Falls du keinen guten Namen für eine Aufzählung finden kannst, hängt das vermutlich damit zusammen, dass die Aufzähler keine semantische Beziehung zueinander haben. In diesem Fall solltest du constexpr Variablen einsetzen.

// bad
enum { red = 0xFF0000, scale = 4, is_signed = 1 };

// good
constexpr int red = 0xFF0000;
constexpr short scale = 4;
constexpr bool is_signed = true;

Enum.7: Specify the underlying type of an enumeration only when necessary

Seit C++11 lässt sich der zugrunde liegende Datentyp der Aufzähler angeben. Das spart gegebenenfalls Speicher. Standardmäßig ist der zugrunde liegende Datentyp einer Aufzählung mit Gültigkeitsbereich int. Damit lassen sich diese vorwärts deklarieren.

// typeEnum.cpp

#include <iostream>

enum class Colour1{
red,
blue,
green
};

enum struct Colour2: char {
red,
blue,
green
};

int main(){

std::cout << sizeof(Colour1) << std::endl; // 4
std::cout << sizeof(Colour2) << std::endl; // 1

}

Enum.8: Specify enumerator values only when necessary

Durch das explizite Setzen der Aufzähler kann es natürlich passieren, dass ein Wert zweimal vergeben wird. Die folgende Aufzählung Col2 hat genau dieses Problem.

enum class Col1 { red, yellow, blue };
enum class Col2 { red = 1, yellow = 2, blue = 2 }; // typo
enum class Month { jan = 1, feb, mar, apr, may, jun, jul, august, sep,
oct, nov, dec }; // starting with 1 is conventional

Dieser Artikel war kurz und bündig. Du solltest aber die Meta-Regel im Kopf behalten: Verwende Aufzählung mit Gültigkeitsbereich!

Das nächste Kapitel der C++ Core Guidelines beschäftigt sich mit den rund 35 Regeln für Ressource Management. Das bedeutet, wir tauchen im nächsten Artikel mitten in den Herz von C++.