Dlaczego klasa enum jest lepsza od zwykłego enum?

429

Słyszałem, że kilka osób zaleca stosowanie enum klas w C ++ ze względu na ich bezpieczeństwo typu .

Ale co to tak naprawdę oznacza?

Ołeksij
źródło
57
Kiedy ktoś twierdzi, że jakaś konstrukcja programistyczna jest „zła”, stara się zniechęcić cię do samodzielnego myślenia.
Pete Becker,
3
@NicolBolas: To jest raczej pytanie retoryczne, które ma dostarczyć odpowiedzi na najczęściej zadawane pytania (czy to naprawdę Często zadawane pytania to inna historia).
David Rodríguez - dribeas
@David, trwa dyskusja, czy powinien to być FAQ, czy nie, która zaczyna się tutaj . Wprowadź powitanie.
sbi
17
@PeteBecker Czasami starają się tylko chronić przed tobą.
piccy
geeksforgeeks.org/... Jest to również miejsce, aby zrozumieć dobry enumvs enum class.
mr_azad

Odpowiedzi:

472

C ++ ma dwa rodzaje enum:

  1. enum classes
  2. Zwykły enums

Oto kilka przykładów, jak je zadeklarować:

 enum class Color { red, green, blue }; // enum class
 enum Animal { dog, cat, bird, human }; // plain enum 

Jaka jest różnica między dwoma?

  • enum classes - nazwy modułu wyliczającego są lokalne dla wyliczenia, a ich wartości nie są domyślnie konwertowane na inne typy (jak inne enumlub int)

  • Zwykłe enums - gdzie nazwy modułu wyliczającego mają ten sam zakres co wyliczenie, a ich wartości domyślnie są konwertowane na liczby całkowite i inne typy

Przykład:

enum Color { red, green, blue };                    // plain enum 
enum Card { red_card, green_card, yellow_card };    // another plain enum 
enum class Animal { dog, deer, cat, bird, human };  // enum class
enum class Mammal { kangaroo, deer, human };        // another enum class

void fun() {

    // examples of bad use of plain enums:
    Color color = Color::red;
    Card card = Card::green_card;

    int num = color;    // no problem

    if (color == Card::red_card) // no problem (bad)
        cout << "bad" << endl;

    if (card == Color::green)   // no problem (bad)
        cout << "bad" << endl;

    // examples of good use of enum classes (safe)
    Animal a = Animal::deer;
    Mammal m = Mammal::deer;

    int num2 = a;   // error
    if (m == a)         // error (good)
        cout << "bad" << endl;

    if (a == Mammal::deer) // error (good)
        cout << "bad" << endl;

}

Wniosek:

enum classes powinny być preferowane, ponieważ powodują mniej niespodzianek, które mogą potencjalnie prowadzić do błędów.

Ołeksij
źródło
7
Dobry przykład ... czy istnieje sposób na połączenie bezpieczeństwa typu wersji klasowej z promocją przestrzeni nazw wersji enum? To znaczy, jeśli mam klasę Aze stanem i tworzę enum class State { online, offline };jako dziecko klasy A, chciałbym zrobić w state == onlineśrodku kontrole Azamiast state == State::online... czy to możliwe?
zaznacz
31
Nie. Promocja przestrzeni nazw to Bad Thing ™, a połowę uzasadnienia enum classstanowiła jej eliminacja.
Puppy
10
W C ++ 11 możesz również używać jawnie wpisanych wyliczeń, takich jak enum Animal: unsigned int {pies, jeleń, kot, ptak}
Blasius Secundus
3
@Cat Plus Plus Rozumiem, że @Oleksiy mówi, że jest źle. Moje pytanie nie brzmiało, czy Oleksiy uważał, że jest złe. Moje pytanie było prośbą o wyszczególnienie, co jest w tym złego. W szczególności dlaczego na przykład Oleksiy uważa źle Color color = Color::red.
chux - Przywróć Monikę
9
@Cat Plus Plus Tak więc zły przykład nie pojawia się aż do if (color == Card::red_card)linii, 4 wiersze później niż komentarz (który widzę teraz dotyczy pierwszej połowy bloku). 2 wiersze bloku dają złe przykłady. Pierwsze 3 linie nie stanowią problemu. „Cały blok jest powodem, dla którego zwykłe wyliczenia są złe”, rzuciły mnie, ponieważ myślałem, że masz na myśli coś z nimi nie tak. Rozumiem teraz, to tylko konfiguracja. W każdym razie dzięki za opinie.
chux - Przywróć Monikę
248

Z FAQ C ++ 11 Bjarne Stroustrup :

The enum class es ( „nowe teksty stałe”, „silne wyliczenia”) dotyczą trzech problemów z tradycyjnym C ++ wyliczenia:

  • konwencjonalne wyliczenia domyślnie przekształcają się w int, powodując błędy, gdy ktoś nie chce, aby wyliczenie działało jako liczba całkowita.
  • konwencjonalne wyliczenia eksportują swoje wyliczniki do otaczającego zakresu, powodując konflikty nazw.
  • enumnie można określić podstawowego typu an , powodując zamieszanie, problemy ze zgodnością i uniemożliwiając deklarację przesyłania dalej.

Nowe wyliczenia są „klasą wyliczania”, ponieważ łączą aspekty tradycyjnych wyliczeń (wartości nazw) z aspektami klas (członkowie o zasięgu i brak konwersji).

Tak więc, jak wspomnieli inni użytkownicy, „silne wyliczenia” uczynią kod bezpieczniejszym.

Podstawowym typem „klasycznego” enumjest typ liczb całkowitych wystarczająco duży, aby pasował do wszystkich wartości enum; to zwykle jest int. Również każdy wyliczony typ musi być zgodny zchar typem całkowitym znakiem lub bez znaku.

Jest to szeroki opis tego, jaki enummusi być typ bazowy, więc każdy kompilator sam podejmie decyzję o typie klasycznym, enuma czasem wynik może być zaskakujący.

Na przykład kilka razy widziałem taki kod:

enum E_MY_FAVOURITE_FRUITS
{
    E_APPLE      = 0x01,
    E_WATERMELON = 0x02,
    E_COCONUT    = 0x04,
    E_STRAWBERRY = 0x08,
    E_CHERRY     = 0x10,
    E_PINEAPPLE  = 0x20,
    E_BANANA     = 0x40,
    E_MANGO      = 0x80,
    E_MY_FAVOURITE_FRUITS_FORCE8 = 0xFF // 'Force' 8bits, how can you tell?
};

W powyższym kodzie, niektórzy naiwni myśląc, że koder jest kompilator będzie przechowywać E_MY_FAVOURITE_FRUITSwartości do unsigned 8bit ... ale nie ma gwarancji o tym: kompilator może wybrać unsigned charlub intczy shortkażdy z tych typów są wystarczająco duże, aby zmieścić wszystkie wartości widoczne w enum. Dodanie pola E_MY_FAVOURITE_FRUITS_FORCE8jest dużym obciążeniem i nie zmusza kompilatora do dokonywania jakichkolwiek wyborów dotyczących podstawowego typu pliku enum.

Jeśli jest jakiś fragment kodu, który opiera się na rozmiarze typu i / lub zakłada, że E_MY_FAVOURITE_FRUITSbędzie miał pewną szerokość (np. Procedury serializacji), ten kod może zachowywać się w dziwny sposób w zależności od myśli kompilatora.

Co gorsza, jeśli jakiś kolega z pracy niedbale doda nową wartość do naszego enum:

    E_DEVIL_FRUIT  = 0x100, // New fruit, with value greater than 8bits

Kompilator nie narzeka! Po prostu zmienia rozmiar, aby pasował do wszystkich wartości enum(przy założeniu, że kompilator używał najmniejszego możliwego typu, co jest założeniem, że nie możemy tego zrobić). Ten prosty i nieostrożny dodatek do enumsubtelności może zepsuć powiązany kod.

Ponieważ C ++ 11 jest możliwe, aby określić typ podstawowy dla ( enumi enum classdzięki rdb ), więc problem ten jest starannie rozwiązany:

enum class E_MY_FAVOURITE_FRUITS : unsigned char
{
    E_APPLE        = 0x01,
    E_WATERMELON   = 0x02,
    E_COCONUT      = 0x04,
    E_STRAWBERRY   = 0x08,
    E_CHERRY       = 0x10,
    E_PINEAPPLE    = 0x20,
    E_BANANA       = 0x40,
    E_MANGO        = 0x80,
    E_DEVIL_FRUIT  = 0x100, // Warning!: constant value truncated
};

Określając typ bazowy, jeśli pole ma wyrażenie spoza zakresu tego typu, kompilator będzie narzekał zamiast zmieniać typ bazowy.

Myślę, że to dobra poprawa bezpieczeństwa.

Dlaczego więc klasa enum jest lepsza od zwykłego enum? , jeśli możemy wybrać typ bazowy dla enum classwyliczeń scoped ( ) i unscoped ( enum), co jeszcze jest enum classlepszym wyborem ?:

  • Nie zamieniają się domyślnie na int.
  • Nie zanieczyszczają otaczającej przestrzeni nazw.
  • Mogą być deklarowane w przód.
PaperBirdMaster
źródło
1
Przypuszczam, że możemy ograniczyć typ bazowy wyliczenia również dla zwykłych wyliczeń, o ile mamy C ++ 11
Sagar Padhye
11
Przepraszamy, ale ta odpowiedź jest błędna. „klasa enum” nie ma nic wspólnego z możliwością określenia typu. Jest to niezależna funkcja, która istnieje zarówno dla regularnych wyliczeń, jak i dla klas wyliczania.
rdb
14
Taka jest umowa: * Klasy Enum są nową funkcją w C ++ 11. * Wyliczone typy są nową funkcją w C ++ 11. Są to dwie odrębne, niepowiązane nowe funkcje w C ++ 11. Możesz użyć obu, albo możesz użyć jednego lub drugiego.
rdb
2
Myślę, że Alex Allain zapewnia najpełniejsze proste wytłumaczenie ja widziałem jeszcze w tym blogu w [ cprogramming.com/c++11/... . Tradycyjne wyliczanie było dobre do używania nazw zamiast wartości całkowitych i unikania używania pre-procesora #defines, co było dobrą rzeczą - dodało przejrzystości. Klasa enum usuwa pojęcie wartości liczbowej modułu wyliczającego oraz wprowadza zakres i silne pisanie, które zwiększa się (cóż, może zwiększyć :-) poprawność programu. To przybliża cię o krok do myślenia o obiektach myślących.
Jon Spencer
2
Nawiasem mówiąc, zawsze jest zabawnie, kiedy przeglądasz kod i nagle dzieje się One Piece .
Justin Time - Przywróć Monikę
47

Podstawową zaletą używania klasy wyliczania w porównaniu do normalnych wyliczeń jest to, że możesz mieć te same zmienne wyliczające dla 2 różnych wyliczeń i nadal możesz je rozwiązywać (co OP określiło jako bezpieczne dla typu )

Na przykład:

enum class Color1 { red, green, blue };    //this will compile
enum class Color2 { red, green, blue };

enum Color1 { red, green, blue };    //this will not compile 
enum Color2 { red, green, blue };

Jeśli chodzi o podstawowe wyliczenia, kompilator nie będzie w stanie rozróżnić, czy redodnosi się do typu, Color1czy Color2jak w poniższej instrukcji.

enum Color1 { red, green, blue };   
enum Color2 { red, green, blue };
int x = red;    //Compile time error(which red are you refering to??)
Saksham
źródło
1
@Oleksiy Ohh Nie przeczytałem twojego pytania poprawnie. Rozważ to dodatek dla tych, którzy nie wiedzieli.
Saksham,
w porządku! Prawie o tym zapomniałem
Oleksiy
oczywiście piszesz enum { COLOR1_RED, COLOR1_GREE, COLOR1_BLUE }, łatwo omijając problemy z przestrzenią nazw. Argument przestrzeni nazw jest jednym z trzech wymienionych tutaj, których w ogóle nie kupuję.
Jo So
2
@Jo So To rozwiązanie jest niepotrzebnym obejściem. ENUM: enum Color1 { COLOR1_RED, COLOR1_GREEN, COLOR1_BLUE }jest porównywalna do klasy ENUM: enum class Color1 { RED, GREEN, BLUE }. Dostęp jest podobny: COLOR1_REDvs Color1::RED, ale wersja Enum wymaga wpisania „COLOR1” w każdej wartości, co daje więcej miejsca na literówki, których unika zachowanie przestrzeni nazw klasy enum.
cdgraham
2
Prosimy o konstruktywną krytykę . Kiedy mówię więcej miejsca na literówki, mam na myśli, kiedy pierwotnie definiujesz wartości enum Color1, których kompilator nie może złapać, ponieważ prawdopodobnie nadal byłby to „poprawna” nazwa. Jeśli piszę RED, GREENi tak dalej, używając klasy wyliczeniowej, to nie może rozwiązać, enum Bananaponieważ wymaga podania Color1::REDw celu uzyskania dostępu do wartości (argument przestrzeni nazw). Nadal są dobre czasy, aby je wykorzystać enum, ale zachowanie przestrzeni nazw enum classczęsto może być bardzo korzystne.
cdgraham
20

Wyliczenia są używane do reprezentowania zestawu wartości całkowitych.

Słowo classkluczowe po enumokreśla, że ​​wyliczenie jest silnie typowane, a jego wyliczacze mają zakres. W ten sposób enumklasy zapobiegają przypadkowemu niewłaściwemu użyciu stałych.

Na przykład:

enum class Animal{Dog, Cat, Tiger};
enum class Pets{Dog, Parrot};

Tutaj nie możemy mieszać wartości zwierząt i zwierząt domowych.

Animal a = Dog;       // Error: which DOG?    
Animal a = Pets::Dog  // Pets::Dog is not an Animal
Alok151290
źródło
7

FAQ w C ++ 11 wymienia poniższe punkty:

konwencjonalne wyliczenia domyślnie przekształcają się w int, powodując błędy, gdy ktoś nie chce, aby wyliczenie działało jako liczba całkowita.

enum color
{
    Red,
    Green,
    Yellow
};

enum class NewColor
{
    Red_1,
    Green_1,
    Yellow_1
};

int main()
{
    //! Implicit conversion is possible
    int i = Red;

    //! Need enum class name followed by access specifier. Ex: NewColor::Red_1
    int j = Red_1; // error C2065: 'Red_1': undeclared identifier

    //! Implicit converison is not possible. Solution Ex: int k = (int)NewColor::Red_1;
    int k = NewColor::Red_1; // error C2440: 'initializing': cannot convert from 'NewColor' to 'int'

    return 0;
}

konwencjonalne wyliczenia eksportują swoje wyliczniki do otaczającego zakresu, powodując konflikty nazw.

// Header.h

enum vehicle
{
    Car,
    Bus,
    Bike,
    Autorickshow
};

enum FourWheeler
{
    Car,        // error C2365: 'Car': redefinition; previous definition was 'enumerator'
    SmallBus
};

enum class Editor
{
    vim,
    eclipes,
    VisualStudio
};

enum class CppEditor
{
    eclipes,       // No error of redefinitions
    VisualStudio,  // No error of redefinitions
    QtCreator
};

Nie można określić podstawowego rodzaju wyliczenia, co powoduje zamieszanie, problemy ze zgodnością i uniemożliwia deklarację przesyłania dalej.

// Header1.h
#include <iostream>

using namespace std;

enum class Port : unsigned char; // Forward declare

class MyClass
{
public:
    void PrintPort(enum class Port p);
};

void MyClass::PrintPort(enum class Port p)
{
    cout << (int)p << endl;
}

.

// Header.h
enum class Port : unsigned char // Declare enum type explicitly
{
    PORT_1 = 0x01,
    PORT_2 = 0x02,
    PORT_3 = 0x04
};

.

// Source.cpp
#include "Header1.h"
#include "Header.h"

using namespace std;
int main()
{
    MyClass m;
    m.PrintPort(Port::PORT_1);

    return 0;
}
Swapnil
źródło
C ++ 11 pozwala także na pisanie „nieklasowych” wyliczeń . Problemy z zanieczyszczeniem przestrzeni nazw itp. Nadal istnieją. Spójrz na odpowiednie odpowiedzi, które istniały na długo przed tym ...
user2864740
7
  1. nie konwertuj domyślnie na int
  2. może wybrać, który typ leży u podstaw
  3. Przestrzeń nazw ENUM, aby uniknąć zanieczyszczenia
  4. W porównaniu z normalną klasą można zadeklarować do przodu, ale nie ma metod
Qinsheng Zhang
źródło
2

Warto zauważyć, oprócz tych innych odpowiedzi, że C ++ 20 rozwiązuje jeden z problemów, który enum classma: gadatliwość. Wyobrażając sobie hipotetyczną enum class, Color.

void foo(Color c)
  switch (c) {
    case Color::Red: ...;
    case Color::Green: ...;
    case Color::Blue: ...;
    // etc
  }
}

Jest to pełne określenie w porównaniu do zwykłego enumwariantu, w którym nazwy mają zasięg globalny i dlatego nie trzeba ich poprzedzać Color::.

Jednak w C ++ 20 możemy using enumwprowadzić wszystkie nazwy w wyliczeniu do bieżącego zakresu, rozwiązując problem.

void foo(Color c)
  using enum Color;
  switch (c) {
    case Red: ...;
    case Green: ...;
    case Blue: ...;
    // etc
  }
}

Więc teraz nie ma powodu, aby nie używać enum class.

Tom VH
źródło
1

Ponieważ, jak powiedziano w innych odpowiedziach, wyliczanie klas nie jest domyślnie konwertowane na int / bool, pomaga to również uniknąć błędnego kodu, takiego jak:

enum MyEnum {
  Value1,
  Value2,
};
...
if (var == Value1 || Value2) // Should be "var == Value2" no error/warning
Arnaud
źródło
2
Aby zakończyć mój poprzedni komentarz, zauważ, że gcc ma teraz ostrzeżenie o nazwie -Wint-in-bool-context, które wychwytuje dokładnie tego rodzaju błędy.
Arnaud,
0

Jednej rzeczy, o której nie wspomniano wprost - funkcja scope daje ci możliwość posiadania tej samej nazwy dla metody wyliczania i klasy. Na przykład:

class Test
{
public:
   // these call ProcessCommand() internally
   void TakeSnapshot();
   void RestoreSnapshot();
private:
   enum class Command // wouldn't be possible without 'class'
   {
        TakeSnapshot,
        RestoreSnapshot
   };
   void ProcessCommand(Command cmd); // signal the other thread or whatever
};
Miro Kropacek
źródło