Namespaces für Aufzählungstypen - Best Practices

102

Oft braucht man mehrere aufgezählte Typen zusammen. Manchmal hat man einen Namenskonflikt. Hierfür kommen zwei Lösungen in den Sinn: Verwenden Sie einen Namespace oder verwenden Sie 'größere' Enum-Elementnamen. Die Namespace-Lösung verfügt jedoch über zwei mögliche Implementierungen: eine Dummy-Klasse mit verschachtelter Aufzählung oder einen vollständigen Namespace.

Ich suche nach Vor- und Nachteilen aller drei Ansätze.

Beispiel:

// oft seen hand-crafted name clash solution
enum eColors { cRed, cColorBlue, cGreen, cYellow, cColorsEnd };
enum eFeelings { cAngry, cFeelingBlue, cHappy, cFeelingsEnd };
void setPenColor( const eColors c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cColorBlue: //...
        //...
    }
 }


// (ab)using a class as a namespace
class Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
class Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
 }


 // a real namespace?
 namespace Colors { enum e { cRed, cBlue, cGreen, cYellow, cEnd }; };
 namespace Feelings { enum e { cAngry, cBlue, cHappy, cEnd }; };
 void setPenColor( const Colors::e c ) {
    switch (c) {
        default: assert(false);
        break; case Colors::cRed: //...
        break; case Colors::cBlue: //...
        //...
    }
  }
xtofl
quelle
19
Zuallererst würde ich Farbe :: Rot, Gefühl: Wütend usw. verwenden
abatishchev
Gute Frage, ich habe die Namespace-Methode verwendet ....;)
MiniScalope
18
Das Präfix "c" für alles beeinträchtigt die Lesbarkeit.
Benutzer
8
@ Benutzer: warum, danke für die Eröffnung der ungarischen Diskussion :)
xtofl
5
Beachten Sie, dass Sie die Aufzählung nicht wie in enum e {...}benennen müssen. Aufzählungen können anonym sein, dh enum {...}, was in Namespace oder Klasse viel sinnvoller ist.
Kralyk

Antworten:

73

Ursprüngliche C ++ 03 Antwort:

Der Vorteil von a namespace(über a class) besteht darin, dass Sie usingDeklarationen verwenden können, wenn Sie möchten.

Das Problem bei der Verwendung von a namespacebesteht darin, dass Namespaces an anderer Stelle im Code erweitert werden können. In einem großen Projekt kann nicht garantiert werden, dass zwei unterschiedliche Aufzählungen nicht glauben, dass sie aufgerufen werdeneFeelings

Für einfacheren Code verwende ich a struct, da Sie vermutlich möchten, dass der Inhalt öffentlich ist.

Wenn Sie eine dieser Praktiken ausführen, sind Sie der Kurve voraus und müssen dies wahrscheinlich nicht weiter untersuchen.

Neuere C ++ 11-Ratschläge:

Wenn Sie C ++ 11 oder höher verwenden, enum classwerden die Aufzählungswerte implizit innerhalb des Aufzählungsnamens erfasst.

Mit enum classverlieren Sie implizite Konvertierungen und Vergleiche mit ganzzahligen Typen, aber in der Praxis kann dies Ihnen helfen, mehrdeutigen oder fehlerhaften Code zu entdecken.

Drew Dormann
quelle
4
Ich stimme der Strukturidee zu. Und danke für das Kompliment :)
xtofl
3
+1 Ich konnte mich nicht an die C ++ 11-Syntax "enum class" erinnern. Ohne diese Funktion sind die Aufzählungen unvollständig.
Grault
Ist es eine Option, "using" für den impliziten Bereich der "enum class" zu verwenden. zB Wird 'using Color :: e;' hinzugefügt? Code die Verwendung von 'cRed' erlauben und wissen, dass dies Color :: e :: cRed sein sollte?
JVApen
20

Zu Ihrer Information In C ++ 0x gibt es eine neue Syntax für Fälle wie den von Ihnen erwähnten (siehe C ++ 0x-Wiki-Seite ).

enum class eColors { ... };
enum class eFeelings { ... };
jmihalicza
quelle
11

Ich habe die vorhergehenden Antworten auf etwa Folgendes hybridisiert: (BEARBEITEN: Dies ist nur für Pre-C ++ 11 nützlich. Wenn Sie C ++ 11 verwenden, verwenden Sie enum class)

Ich habe eine große Header-Datei, die alle meine Projektaufzählungen enthält, da diese Aufzählungen von Arbeiterklassen gemeinsam genutzt werden und es nicht sinnvoll ist, die Aufzählungen in die Arbeiterklassen selbst einzufügen.

Das structvermeidet die Öffentlichkeit: syntaktischen Zucker, und das typedeflässt Sie tatsächlich Variablen dieser Aufzählungen innerhalb anderer Arbeiterklassen deklarieren.

Ich denke nicht, dass die Verwendung eines Namespace überhaupt hilft. Vielleicht ist dies , weil ich ein C # Programmierer sind, und da Sie haben , um den Aufzählungstyp Namen zu verwenden , wenn die Werte Bezug genommen wird , so dass ich es gewohnt bin.

    struct KeySource {
        typedef enum { 
            None, 
            Efuse, 
            Bbram
        } Type;
    };

    struct Checksum {
        typedef enum {
            None =0,
            MD5 = 1,
            SHA1 = 2,
            SHA2 = 3
        } Type;
    };

    struct Encryption {
        typedef enum {
            Undetermined,
            None,
            AES
        } Type;
    };

    struct File {
        typedef enum {
            Unknown = 0,
            MCS,
            MEM,
            BIN,
            HEX
        } Type;
    };

...

class Worker {
    File::Type fileType;
    void DoIt() {
       switch(fileType) {
       case File::MCS: ... ;
       case File::MEM: ... ;
       case File::HEX: ... ;
    }
}
Mark Lakata
quelle
9

Ich würde definitiv vermeiden, eine Klasse dafür zu verwenden; Verwenden Sie stattdessen einen Namespace. Die Frage läuft darauf hinaus, ob ein Namespace oder eindeutige IDs für die Enum-Werte verwendet werden sollen. Persönlich würde ich einen Namespace verwenden, damit meine IDs kürzer und hoffentlich selbsterklärender sind. Dann könnte der Anwendungscode eine Direktive "using namespace" verwenden und alles lesbarer machen.

Aus Ihrem obigen Beispiel:

using namespace Colors;

void setPenColor( const e c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cBlue: //...
        //...
    }
}
Charles Anderson
quelle
Können Sie einen Hinweis geben, warum Sie einen Namespace einer Klasse vorziehen würden?
xtofl
@xtofl: Sie können nicht "mit Klasse Farben" schreiben
MSalters
2
@MSalters: Sie können auch nicht schreiben Colors someColor = Red;, da der Namespace keinen Typ darstellt. Sie müssten Colors::e someColor = Red;stattdessen schreiben , was ziemlich kontraintuitiv ist.
SasQ
@SasQ Müssen Sie nicht Colors::e someColorauch mit a verwenden, struct/classwenn Sie es in einer switchAnweisung verwenden möchten ? Wenn Sie eine anonyme verwenden, kann enumder Switch a nicht auswerten struct.
Macbeths Rätsel
1
Sorry, const e cscheint mir aber kaum lesbar zu sein :-) Tu das nicht. Die Verwendung eines Namespace ist jedoch in Ordnung.
Dhaumann
7

Ein Unterschied zwischen der Verwendung einer Klasse oder eines Namespace besteht darin, dass die Klasse nicht wie ein Namespace erneut geöffnet werden kann. Dies vermeidet die Möglichkeit, dass der Namespace in Zukunft missbraucht wird, aber es gibt auch das Problem, das Sie auch nicht zu den Aufzählungen hinzufügen können.

Ein möglicher Vorteil bei der Verwendung einer Klasse besteht darin, dass sie als Argumente vom Typ Vorlage verwendet werden können, was bei Namespaces nicht der Fall ist:

class Colors {
public:
  enum TYPE {
    Red,
    Green,
    Blue
  };
};

template <typename T> void foo (T t) {
  typedef typename T::TYPE EnumType;
  // ...
}

Persönlich bin ich kein Fan von Verwendung , und ich bevorzuge die vollqualifizierten Namen, daher sehe ich das nicht wirklich als Plus für Namespaces. Dies ist jedoch wahrscheinlich nicht die wichtigste Entscheidung, die Sie in Ihrem Projekt treffen werden!

Richard Corden
quelle
Das Nichteröffnen von Klassen ist ebenfalls ein potenzieller Nachteil. Die Liste der Farben ist auch nicht endlich.
MSalters
1
Ich denke, dass es ein potenzieller Vorteil ist, eine Klasse nicht zu erneuern. Wenn ich mehr Farben möchte, würde ich die Klasse einfach mit mehr Farben neu kompilieren. Wenn ich das nicht kann (sagen wir, ich habe den Code nicht), dann möchte ich ihn auf keinen Fall berühren.
Thomas Eding
@MSalters: Keine Möglichkeit, eine Klasse erneut zu öffnen, ist nicht nur kein Nachteil, sondern auch ein Sicherheitstool. Denn wenn es möglich wäre, eine Klasse erneut zu öffnen und der Aufzählung einige Werte hinzuzufügen, könnte dies anderen Bibliothekscode beschädigen, der bereits von dieser Aufzählung abhängt und nur den alten Wertesatz kennt. Es würde dann gerne diese neuen Werte akzeptieren, aber zur Laufzeit davon abhalten, nicht zu wissen, was mit ihnen zu tun ist. Denken Sie daran, dass Open-Closed-Prinzip: Die Klasse sollte zum Ändern geschlossen , aber zum Erweitern geöffnet sein . Mit Erweitern meine ich, nicht zum vorhandenen Code hinzuzufügen, sondern ihn mit dem neuen Code zu umschließen (z. B. Ableiten).
SasQ
Wenn Sie also die Aufzählung erweitern möchten, sollten Sie sie zu einem neuen Typ machen, der vom ersten abgeleitet ist (wenn dies nur in C ++ leicht möglich wäre ...; /). Dann könnte es sicher von dem neuen Code verwendet werden, der diese neuen Werte versteht, aber nur die alten Werte würden vom alten Code akzeptiert (indem sie nach unten konvertiert werden). Sie sollten niemanden von diesen neuen Werten als vom falschen Typ (den erweiterten) akzeptieren. Nur die alten Werte, die sie verstehen, werden als vom richtigen (Basis-) Typ akzeptiert (und versehentlich auch vom neuen Typ, so dass sie auch von neuem Code akzeptiert werden können).
SasQ
7

Der Vorteil der Verwendung einer Klasse besteht darin, dass Sie eine vollwertige Klasse darauf aufbauen können.

#include <cassert>

class Color
{
public:
    typedef enum
    {
        Red,
        Blue,
        Green,
        Yellow
    } enum_type;

private:
    enum_type _val;

public:
    Color(enum_type val = Blue)
        : _val(val)
    {
        assert(val <= Yellow);
    }

    operator enum_type() const
    {
        return _val;
    }
};

void SetPenColor(const Color c)
{
    switch (c)
    {
        case Color::Red:
            // ...
            break;
    }
}

Wie das obige Beispiel zeigt, können Sie mit einer Klasse:

  1. verbieten (leider nicht zur Kompilierungszeit) C ++, eine Umwandlung von ungültigem Wert zuzulassen,
  2. Legen Sie einen Standardwert (ungleich Null) für neu erstellte Aufzählungen fest.
  3. Fügen Sie weitere Methoden hinzu, z. B. zum Zurückgeben einer Zeichenfolgendarstellung einer Auswahl.

Beachten Sie nur, dass Sie deklarieren müssen, operator enum_type()damit C ++ weiß, wie Ihre Klasse in eine zugrunde liegende Aufzählung konvertiert wird. Andernfalls können Sie den Typ nicht an eine switchAnweisung übergeben.

Michał Górny
quelle
Hat diese Lösung etwas mit dem zu tun , was hier gezeigt wird?: En.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Safe_Enum Ich denke darüber nach, wie ich daraus eine Vorlage machen kann, die ich nicht jedes Mal neu schreiben muss Ich muss es benutzen.
SasQ
@ SasQ: Es scheint ähnlich, ja. Das ist wahrscheinlich die gleiche Idee. Ich bin mir jedoch nicht sicher, ob eine Vorlage von Vorteil ist, es sei denn, Sie fügen dort viele gängige Methoden hinzu.
Michał Górny
1. Nicht wirklich wahr. Sie können die Kompilierungszeit überprüfen lassen, ob die Aufzählung gültig ist, über const_expr oder über den privaten Konstruktor für ein int.
Xryl669
5

Da Aufzählungen auf ihren umschließenden Bereich beschränkt sind, ist es wahrscheinlich am besten, sie in etwas einzuwickeln , um eine Verschmutzung des globalen Namespace und eine Vermeidung von Namenskollisionen zu vermeiden. Ich bevorzuge einen Namespace gegenüber dem Unterricht, nur weil er namespacesich wie ein Sack voll Halt classanfühlt , während er sich wie ein robustes Objekt anfühlt (vgl. Die structvs.- classDebatte). Ein möglicher Vorteil eines Namespace besteht darin, dass er später erweitert werden kann - nützlich, wenn Sie mit Code von Drittanbietern arbeiten, den Sie nicht ändern können.

Dies ist natürlich alles umstritten, wenn wir Enum-Klassen mit C ++ 0x erhalten.

Michael Kristofik
quelle
Aufzählungsklassen ... müssen das nachschlagen!
xtofl
3

Ich neige auch dazu, meine Aufzählungen in Klassen zu wickeln.

Wie von Richard Corden signalisiert, besteht der Vorteil einer Klasse darin, dass es sich um einen Typ im c ++ - Sinne handelt und Sie ihn daher mit Vorlagen verwenden können.

Ich habe eine spezielle Toolbox :: Enum-Klasse für meine Anforderungen, die ich auf alle Vorlagen spezialisiert habe, die grundlegende Funktionen bereitstellen (hauptsächlich: Zuordnen eines Enum-Werts zu einem std :: string, damit E / A leichter zu lesen sind).

Meine kleine Vorlage hat auch den zusätzlichen Vorteil, dass sie wirklich nach den zulässigen Werten sucht. Der Compiler prüft, ob der Wert wirklich in der Aufzählung enthalten ist:

typedef enum { False: 0, True: 2 } boolean;
   // The classic enum you don't want to see around your code ;)

int main(int argc, char* argv[])
{
  boolean x = static_cast<boolean>(1);
  return (x == False || x == True) ? 0 : 1;
} // main

Es hat mich immer gestört, dass der Compiler dies nicht abfängt, da Sie einen Aufzählungswert haben, der keinen Sinn hat (und den Sie nicht erwarten).

Ähnlich:

typedef enum { Zero: 0, One: 1, Two: 2 } example;

int main(int argc, char* argv[])
{
  example y = static_cast<example>(3);
  return (y == Zero || y == One || y == Two) ? 0 : 1;
} // main

Erneut gibt main einen Fehler zurück.

Das Problem ist, dass der Compiler die Aufzählung in die kleinste verfügbare Darstellung einfügt (hier benötigen wir 2 Bits) und dass alles, was in diese Darstellung passt, als gültiger Wert betrachtet wird.

Es gibt auch das Problem, dass Sie manchmal lieber eine Schleife für die möglichen Werte anstelle eines Schalters haben möchten, damit Sie nicht jedes Mal, wenn Sie der Aufzählung einen Wert hinzufügen, alle Schalter ändern müssen.

Alles in allem erleichtert mein kleiner Helfer meine Aufzählungen wirklich (natürlich erhöht dies den Aufwand) und es ist nur möglich, weil ich jede Aufzählung in einer eigenen Struktur verschachtele :)

Matthieu M.
quelle
4
Interessant. Haben Sie etwas dagegen, die Definition Ihrer Enum-Klasse zu teilen?
Momeara