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?
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?
Odpowiedzi:
C ++ ma dwa rodzaje enum
:
enum class
esenum
sOto 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 class
es - nazwy modułu wyliczającego są lokalne dla wyliczenia, a ich wartości nie są domyślnie konwertowane na inne typy (jak inne enum
lub int
)
Zwykłe enum
s - 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;
}
enum class
es powinny być preferowane, ponieważ powodują mniej niespodzianek, które mogą potencjalnie prowadzić do błędów.
A
ze stanem i tworzę enum class State { online, offline };
jako dziecko klasy A
, chciałbym zrobić w state == online
środku kontrole A
zamiast state == State::online
... czy to możliwe?
enum class
stanowiła jej eliminacja.
Color color = Color::red
.
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.
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.
enum
nie 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” enum
jest 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 enum
musi być typ bazowy, więc każdy kompilator sam podejmie decyzję o typie klasycznym, enum
a 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_FRUITS
wartości do unsigned 8bit ... ale nie ma gwarancji o tym: kompilator może wybrać unsigned char
lub int
czy short
każ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_FORCE8
jest 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_FRUITS
bę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 enum
subtelności może zepsuć powiązany kod.
Ponieważ C ++ 11 jest możliwe, aby określić typ podstawowy dla ( enum
i enum class
dzię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 class
wyliczeń scoped ( ) i unscoped ( enum
), co jeszcze jest enum class
lepszym wyborem ?:
int
.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 red
odnosi się do typu, Color1
czy Color2
jak 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??)
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ę.
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_RED
vs 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.
enum Color1
, których kompilator nie może złapać, ponieważ prawdopodobnie nadal byłby to „poprawna” nazwa. Jeśli piszę RED
, GREEN
i tak dalej, używając klasy wyliczeniowej, to nie może rozwiązać, enum Banana
ponieważ wymaga podania Color1::RED
w celu uzyskania dostępu do wartości (argument przestrzeni nazw). Nadal są dobre czasy, aby je wykorzystać enum
, ale zachowanie przestrzeni nazw enum class
często może być bardzo korzystne.
Wyliczenia są używane do reprezentowania zestawu wartości całkowitych.
Słowo class
kluczowe po enum
określa, że wyliczenie jest silnie typowane, a jego wyliczacze mają zakres. W ten sposób enum
klasy 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
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;
}
Warto zauważyć, oprócz tych innych odpowiedzi, że C ++ 20 rozwiązuje jeden z problemów, który enum class
ma: 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 enum
wariantu, w którym nazwy mają zasięg globalny i dlatego nie trzeba ich poprzedzać Color::
.
Jednak w C ++ 20 możemy using enum
wprowadzić 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
.
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
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
};