przestrzenie nazw dla typów wyliczeniowych - najlepsze praktyki

104

Często potrzeba razem kilku wyliczonych typów. Czasami zdarza się konflikt nazw. Przychodzą mi na myśl dwa rozwiązania: użyj przestrzeni nazw lub użyj „większych” nazw elementów wyliczenia. Mimo to rozwiązanie przestrzeni nazw ma dwie możliwe implementacje: klasę fikcyjną z zagnieżdżonym wyliczeniem lub pełną przestrzeń nazw.

Szukam zalet i wad wszystkich trzech podejść.

Przykład:

// 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
źródło
19
Przede wszystkim
użyłbym
dobre pytanie, użyłem metody namespace ....;)
MiniScalope
18
przedrostek „c” na wszystkim szkodzi czytelności.
Użytkownik
8
@User: dlaczego, dziękuję za rozpoczęcie węgierskiej dyskusji :)
xtofl
5
Zwróć uwagę, że nie musisz nazywać wyliczenia, ponieważ wyliczenia enum e {...}mogą być anonimowe, tj. enum {...}Co ma znacznie większy sens, gdy jest opakowane w przestrzeń nazw lub klasę.
kralyk

Odpowiedzi:

73

Oryginalna odpowiedź C ++ 03:

Korzyść z punktu A namespace(ponad class) jest to, że można użyć usingdeklaracji, kiedy chcesz.

Problem z użyciem namespacejest to, że przestrzenie nazw można rozszerzyć gdzie indziej w kodzie. W dużym projekcie nie można mieć gwarancji, że dwie odrębne wyliczenia nie myślą, że są wywoływaneeFeelings

Aby uzyskać prostszy kod, używam a struct, ponieważ prawdopodobnie chcesz, aby zawartość była publiczna.

Jeśli wykonujesz którąkolwiek z tych praktyk, wyprzedzasz konkurencję i prawdopodobnie nie musisz tego dalej analizować.

Nowsze porady dotyczące języka C ++ 11:

Jeśli używasz C ++ 11 lub nowszego, enum classniejawnie zakres wartości wyliczenia w nazwie wyliczenia.

Dzięki enum classtemu utracisz niejawne konwersje i porównania z typami całkowitymi, ale w praktyce może to pomóc w odkryciu niejednoznacznego lub błędnego kodu.

Drew Dormann
źródło
4
Zgadzam się z ideą struktury. I dzięki za komplement :)
xtofl
3
+1 Nie pamiętam składni „klasy wyliczeniowej” w C ++ 11. Bez tej funkcji wyliczenia są niekompletne.
Grault
Jest ich opcją użycia „using” dla niejawnego zakresu „klasy wyliczeniowej”. np. doda „using Color :: e;” aby kod zezwalał na użycie 'cRed' i czy powinien to być Color :: e :: cRed?
JVA Otwarty
20

FYI W C ++ 0x jest nowa składnia dla przypadków takich jak to, o czym wspomniałeś (patrz strona wiki C ++ 0x )

enum class eColors { ... };
enum class eFeelings { ... };
jmihalicza
źródło
11

Powyższe odpowiedzi zhybrydyzowałem z czymś takim: (EDYCJA: jest to przydatne tylko dla wersji przed C ++ 11. Jeśli używasz C ++ 11, użyj enum class)

Mam jeden duży plik nagłówkowy, który zawiera wszystkie wyliczenia mojego projektu, ponieważ te wyliczenia są współdzielone między klasami roboczymi i nie ma sensu umieszczać wyliczeń w samych klasach roboczych.

structUnika publicznego: cukier syntaktyczny i typedefpozwala właściwie zadeklarować zmienne te teksty stałe wewnątrz innych klas robotniczych.

Myślę, że używanie przestrzeni nazw w ogóle nie pomaga. Może to dlatego, że jestem programista C #, i tam mają korzystać z nazwy typu enum, odnosząc wartości, więc jestem do tego przyzwyczajony.

    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
źródło
9

Zdecydowanie unikałbym używania do tego klasy; zamiast tego użyj przestrzeni nazw. Pytanie sprowadza się do tego, czy użyć przestrzeni nazw, czy użyć unikalnych identyfikatorów dla wartości wyliczeniowych. Osobiście użyłbym przestrzeni nazw, aby moje identyfikatory były krótsze i, miejmy nadzieję, bardziej zrozumiałe. Następnie kod aplikacji mógłby użyć dyrektywy „using namespace” i uczynić wszystko bardziej czytelnym.

Z powyższego przykładu:

using namespace Colors;

void setPenColor( const e c ) {
    switch (c) {
        default: assert(false);
        break; case cRed: //...
        break; case cBlue: //...
        //...
    }
}
Charles Anderson
źródło
Czy mógłbyś podpowiedzieć, dlaczego wolisz przestrzeń nazw od klasy?
xtofl
@xtofl: nie możesz pisać „używając klas kolorów”
MSalters
2
@MSalters: Nie możesz też pisać Colors someColor = Red;, ponieważ przestrzeń nazw nie stanowi typu. Zamiast tego musiałbyś pisać Colors::e someColor = Red;, co jest dość sprzeczne z intuicją.
SasQ
@SasQ Czy nie musiałbyś używać Colors::e someColornawet z a struct/class, gdybyś chciał użyć go w switchoświadczeniu? Jeśli używasz anonimowego, enumprzełącznik nie byłby w stanie ocenić pliku struct.
Macbeth's Enigma
1
Przepraszam, ale const e cwydaje mi się mało czytelny :-) Nie rób tego. Jednak użycie przestrzeni nazw jest w porządku.
dhaumann
7

Różnica między używaniem klasy lub przestrzeni nazw polega na tym, że klasy nie można ponownie otworzyć, tak jak w przypadku przestrzeni nazw. Pozwala to uniknąć możliwości nadużywania przestrzeni nazw w przyszłości, ale istnieje również problem, którego nie można dodać do zestawu wyliczeń.

Możliwą korzyścią z używania klasy jest to, że można ich używać jako argumentów typu szablonu, co nie ma miejsca w przypadku przestrzeni nazw:

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

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

Osobiście nie jestem fanem używania i wolę w pełni kwalifikowane nazwy, więc nie uważam tego za plus dla przestrzeni nazw. Jednak prawdopodobnie nie jest to najważniejsza decyzja, jaką podejmiesz w swoim projekcie!

Richard Corden
źródło
Brak ponownego otwierania zajęć jest również potencjalną wadą. Lista kolorów też nie jest skończona.
MSalters
1
Myślę, że nie repoowanie klasy jest potencjalną zaletą. Gdybym chciał mieć więcej kolorów, po prostu przekompilowałbym klasę z większą liczbą kolorów. Jeśli nie mogę tego zrobić (powiedz, że nie mam kodu), to w żadnym wypadku nie chcę go dotykać.
Thomas Eding
@MSalters: Brak możliwości ponownego otwarcia zajęć to nie tylko wada, ale także narzędzie bezpieczeństwa. Ponieważ gdy byłoby możliwe ponowne otwarcie klasy i dodanie pewnych wartości do wyliczenia, mogłoby to zepsuć inny kod biblioteki, który już zależy od tego wyliczenia i zna tylko stary zestaw wartości. Wtedy z radością zaakceptowałoby te nowe wartości, ale w czasie wykonywania przerwałoby od braku wiedzy, co z nimi zrobić. Zapamiętaj zasadę Open-Closed: klasa powinna być zamknięta na potrzeby modyfikacji , ale otwarta na rozszerzanie . Rozszerzając mam na myśli nie dodawanie do istniejącego kodu, ale zawijanie go nowym kodem (np. Wyprowadzanie).
SasQ
Więc jeśli chcesz rozszerzyć wyliczenie, powinieneś uczynić go nowym typem pochodnym od pierwszego (gdyby tylko było to łatwo możliwe w C ++ ...; /). Wtedy mógłby być bezpiecznie użyty przez nowy kod, który rozumie te nowe wartości, ale tylko stare wartości byłyby akceptowane (poprzez ich konwersję) przez stary kod. Nie powinni akceptować żadnej z tych nowych wartości jako niewłaściwego typu (rozszerzonego). Tylko stare wartości, które rozumieją, są akceptowane jako właściwe (bazowe) typu (i przypadkowo również należą do nowego typu, więc można je również zaakceptować przez nowy kod).
SasQ
7

Zaletą korzystania z klasy jest to, że można na niej zbudować pełnoprawną klasę.

#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;
    }
}

Jak pokazuje powyższy przykład, używając klasy możesz:

  1. zabrania (niestety, nie kompilacji) C ++ zezwalania na rzutowanie z nieprawidłowej wartości,
  2. ustawić (niezerową) wartość domyślną dla nowo utworzonych wyliczeń,
  3. dodaj dalsze metody, takie jak zwracanie łańcucha reprezentującego wybór.

Zwróć uwagę, że musisz zadeklarować operator enum_type(), aby C ++ wiedział, jak przekonwertować twoją klasę na podstawowe wyliczenie. W przeciwnym razie nie będzie można przekazać typu do switchinstrukcji.

Michał Górny
źródło
Czy to rozwiązanie jest w jakiś sposób związane z tym, co tutaj pokazano ?: en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Safe_Enum Zastanawiam się, jak zrobić z tego szablon, że nie muszę za każdym razem przepisywać tego wzorca Muszę tego użyć.
SasQ
@SasQ: wydaje się podobny, tak. To prawdopodobnie ten sam pomysł. Jednak nie jestem pewien, czy szablon jest korzystny, chyba że dodajesz tam wiele „typowych” metod.
Michał Górny
1. Nie do końca prawda. Możesz sprawdzić czas kompilacji, czy wyliczenie jest prawidłowe, za pomocą const_expr lub prywatnego konstruktora dla int.
xryl669
5

Ponieważ wyliczenia są ograniczone do otaczającego zakresu, prawdopodobnie najlepiej jest je opakować w coś, aby uniknąć zanieczyszczenia globalnej przestrzeni nazw i uniknąć kolizji nazw. Wolę przestrzeń nazw od klasy po prostu dlatego, że namespaceczuję się jak worek do przechowywania, podczas gdy classczuje się jak solidny obiekt (por. Debata structvs. classdebata). Możliwą korzyścią dla przestrzeni nazw jest to, że można ją później rozszerzyć - przydatne, jeśli masz do czynienia z kodem innej firmy, którego nie możesz modyfikować.

To wszystko jest oczywiście dyskusyjne, gdy otrzymujemy klasy enum w C ++ 0x.

Michael Kristofik
źródło
klasy enum ... trzeba to sprawdzić!
xtofl
3

Mam też tendencję do owijania wyliczeń na zajęcia.

Jak zasygnalizował Richard Corden, zaletą klasy jest to, że jest to typ w sensie c ++, więc można go używać z szablonami.

Mam specjalny zestaw narzędzi :: Enum dla moich potrzeb, który specjalizuję się w każdym szablonie, który zapewnia podstawowe funkcje (głównie: mapowanie wartości wyliczenia na std :: string, aby I / O były łatwiejsze do odczytania).

Mój mały szablon ma również tę dodatkową zaletę, że naprawdę sprawdza dozwolone wartości. Kompilator jest trochę luźny, jeśli chodzi o sprawdzanie, czy wartość naprawdę znajduje się w wyliczeniu:

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

Zawsze przeszkadzało mi, że kompilator tego nie wychwyci, ponieważ pozostaje wartość wyliczenia, która nie ma sensu (i której się nie spodziewasz).

Podobnie:

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

Ponownie main zwróci błąd.

Problem polega na tym, że kompilator dopasuje wyliczenie do najmniejszej dostępnej reprezentacji (tutaj potrzebujemy 2 bitów) i że wszystko, co pasuje do tej reprezentacji, jest uważane za poprawną wartość.

Istnieje również problem polegający na tym, że czasami wolisz mieć pętlę na możliwych wartościach zamiast przełącznika, aby nie trzeba było modyfikować wszystkich przełączników za każdym razem, gdy dodajesz wartość do wyliczenia.

Podsumowując, mój mały pomocnik naprawdę ułatwia pracę moim wyliczeniom (oczywiście dodaje trochę narzutów) i jest to możliwe tylko dlatego, że zagnieżdżam każdą wyliczenie w jego własnej strukturze :)

Matthieu M.
źródło
4
Ciekawy. Czy możesz podzielić się definicją swojej klasy Enum?
momeara