Do czego służy enum
Struktura grupuje kilka powiązanych wartości w jeden obiekt. enum rozwiązuje inny problem: nadaje nazwy małemu, stałemu zestawowi wyborów. Zamiast pamiętać, że 0 oznacza "czerwony", 1 "zielony", a 2 "niebieski", piszesz Color::Red, a kompilator pilnuje, żeby wszystko się zgadzało.
Sięganie po enum zawsze, gdy zmienna może przyjąć tylko jeden z kilku nazwanych stanów (kolor sygnalizacji, kolor karty, status połączenia), sprawia, że kod sam się dokumentuje, a kompilator wyłapuje literówki i brakujące przypadki, których gołe liczby całkowite nigdy by nie wychwyciły.
Deklarowanie enuma
Współczesny C++ ma dwie odmiany. Zacznij od tej, po którą powinno się sięgać niemal zawsze: enum class z zasięgiem. Wymieniasz nazwy, a do każdego enumeratora odwołujesz się przez nazwę enuma i :::
Zauważ, że piszesz Color::Green, nigdy samo Green. Same wartości to tylko etykiety: porównujesz je, przypisujesz i przekazujesz dalej, ale rzadko interesuje cię liczba pod spodem. Domyślnie Red to 0, Green to 1, a Blue to 2, licząc od zera.
Zwykły enum a enum class
Starszy enum bez zasięgu (bez słowa kluczowego class) wrzuca swoje nazwy prosto do otaczającego zasięgu i sam konwertuje się na int. Brzmi wygodnie, ale powoduje dwa realne problemy:
enum Color { Red, Green, Blue };
enum Fruit { Apple, Banana, Red }; // error: 'Red' already declared
enum Status { Active, Inactive };
int x = Active; // compiles silently - is this what you meant?
if (Active == Banana) { // compares unrelated enums via int - allowed!
}
Ponieważ zwykłe enumeratory to nazwy globalne, dwa enumy mogą się zderzyć tylko dlatego, że mają wspólną etykietę. A ponieważ rozpadają się do int, kompilator chętnie porównuje wartości z zupełnie niepowiązanych enumów. enum class z zasięgiem naprawia oba problemy: nazwy żyją wewnątrz typu, a typ nie zamieni się po cichu w int:
Praktyczna zasada: domyślnie używaj enum class. Po zwykły enum sięgaj tylko wtedy, gdy celowo chcesz niejawnej konwersji na int, na przykład przy starych stałych flag w stylu C.
Własne wartości i typ bazowy
Enumeratorom możesz przypisać jawne liczby. Te, które pominiesz, liczą dalej od poprzedniej wartości, co przydaje się przy kodach statusu HTTP albo flagach bitowych:
Każdy enum opiera się na typie całkowitym, domyślnie int. Możesz go przypiąć do mniejszego typu, gdy liczy się rozmiar, na przykład przy przechowywaniu wielu enumów w upakowanej strukturze albo dopasowaniu do formatu przesyłu danych:
Wybór typu bazowego gwarantuje też zakres, w którym muszą się zmieścić twoje wartości: enum oparty na uint8_t nie pomieści wartości większej niż 255.
Konwersja między enum a int
enum class z zasięgiem nigdy nie konwertuje się niejawnie i o to właśnie chodzi. Gdy naprawdę potrzebujesz liczby (aby ją wypisać, zaindeksować tablicę albo odczytać z pliku), sięgnij po static_cast. Przejście z enum na int jest zawsze bezpieczne:
Zamiana int z powrotem na enum to niebezpieczny kierunek. Rzutowanie nie sprawdza, czy liczba odpowiada prawdziwemu enumeratorowi: zwróci wartość, która technicznie leży poza nazwanym zestawem enuma:
Suit s = static_cast<Suit>(2); // fine - that's Clubs
Suit bad = static_cast<Suit>(99); // compiles, but 99 is not a valid Suit
// using `bad` in a switch or as an array index is a lurking bug
Jeśli liczba pochodzi od użytkownika albo z pliku, samodzielnie sprawdź zakres przed rzutowaniem. Inaczej utworzysz wartość, której nie obsługuje żaden case, co jest subtelnym źródłem niezdefiniowanego zachowania w dalszej części programu.
Enumy i switch
Ponieważ enum to "jedna z ustalonego zestawu wartości", idealnie pasuje do switch. Gdy obsłużysz każdy enumerator, wiele kompilatorów ostrzeże cię, jeśli później dodasz nową wartość i zapomnisz ją obsłużyć. To darmowe bezpieczeństwo, którego gołe liczby całkowite nie dają:
Jedna pułapka: nie ma wbudowanego sposobu na wypisanie nazwy enumeratora. cout << TrafficLight::Red nie skompiluje się dla enuma z zasięgiem, a nawet dla zwykłego enuma wypisze liczbę, a nie "Red". Mały switch albo tablica odnośników, jak powyżej, to zwykły sposób na zamianę enuma w czytelny napis.
Dalej: wyjątki
Enumy i struktury pozwalają modelować, jak wyglądają twoje dane. Prawdziwe programy muszą jednak radzić sobie też z sytuacjami, gdy coś pójdzie nie tak: plik się nie otworzy, liczby nie da się sparsować, wartość wyjdzie poza zakres. C++ obsługuje takie ścieżki błędów za pomocą wyjątków i o nich jest następna strona.
Najczęściej zadawane pytania
Jaka jest różnica między enum a enum class w C++?
Zwykły enum wypuszcza swoje nazwy do otaczającego zasięgu i niejawnie konwertuje się na int, co powoduje konflikty nazw i przypadkowe porównania. enum class z zasięgiem trzyma nazwy wewnątrz typu (Color::Red) i nie konwertuje się na int bez jawnego rzutowania. We współczesnym C++ wybieraj enum class: jest bezpieczny typowo i unika klasycznych pułapek.
Jak zamienić enum na int w C++?
Zwykły enum konwertuje się niejawnie, więc int n = Red; po prostu działa. enum class z zasięgiem wymaga jawnego rzutowania: int n = static_cast<int>(Color::Red);. W drugą stronę rzutujesz liczbę z powrotem: Color c = static_cast<Color>(2);, ale uważaj, bo w czasie działania nikt nie sprawdza, czy wartość to poprawny enumerator.
Od jakiej wartości zaczyna się pierwszy enumerator w C++?
Domyślnie pierwszy enumerator ma wartość 0, a każdy kolejny jest o jeden większy od poprzedniego. W enum class Level { Low, Mid, High }; Low to 0, Mid to 1, a High to 2. Każdemu z nich możesz przypisać jawną wartość, aby to zmienić.