Chociaż wyliczenia bez zakresu są odrębnymi typami w C++, nie są bezpieczne dla typów, a w niektórych przypadkach pozwalają na robienie rzeczy, które nie mają sensu. Rozważmy następujący przypadek:
#include <iostream>
int main()
{
enum Color
{
red,
blue,
};
enum Fruit
{
banana,
apple,
};
Color color { red };
Fruit fruit { banana };
if (color == fruit) // The compiler will compare color and fruit as integers
std::cout << "color and fruit are equal\n"; // and find they are equal!
else
std::cout << "color and fruit are not equal\n";
return 0;
}Wypisuje:
color and fruit are equal
Po wywołaniu color i fruit są porównywane, kompilator sprawdzi, czy wie, jak porównać a Color oraz a Fruit. Tak nie jest. Następnie spróbuje przekonwertować Color i/lub Fruit na liczby całkowite, aby sprawdzić, czy uda mu się znaleźć dopasowanie. Ostatecznie kompilator ustali, że jeśli skonwertuje obie liczby na liczby całkowite, będzie mógł przeprowadzić porównanie. Ponieważ color i fruit są ustawione na moduły wyliczające, które konwertują na wartość całkowitą, 0, color będą równe fruit.
To nie ma sensu semantycznie, ponieważ color i fruit pochodzą z różnych wyliczeń i nie mają być porównywalne. W przypadku standardowych modułów wyliczających nie można w łatwy sposób temu zapobiec.
Ze względu na takie wyzwania, a także problem z zanieczyszczeniem przestrzeni nazw (wyliczenia bez zakresu zdefiniowane w zasięgu globalnym umieszczają swoje wyliczacze w globalnej przestrzeni nazw), projektanci C++ ustalili, że przydałoby się czystsze rozwiązanie dla wyliczeń.
Wyliczenia o zakresie
To rozwiązanie ma ograniczony zakres enumeration (często nazywana klasą wyliczeniową w C++ z powodów, które wkrótce staną się oczywiste).
Wyliczenia z zakresem działają podobnie do wyliczeń bez zakresu (13.2 — Wyliczenia bez zakresu), ale mają dwie podstawowe różnice: nie są domyślnie konwertowane na liczby całkowite, a moduły wyliczające are tylko umieszczane w obszarze zakresu wyliczenia (a nie w regionie zasięgu, w którym znajduje się wyliczenie zdefiniowane).
Aby dokonać wyliczenia zakresowego, używamy słów kluczowych enum class. Pozostała część definicji wyliczenia o zakresie jest taka sama jak definicja wyliczenia bez zakresu. Oto przykład:
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enumeration rather than an unscoped enumeration
{
red, // red is considered part of Color's scope region
blue,
};
enum class Fruit
{
banana, // banana is considered part of Fruit's scope region
apple,
};
Color color { Color::red }; // note: red is not directly accessible, we have to use Color::red
Fruit fruit { Fruit::banana }; // note: banana is not directly accessible, we have to use Fruit::banana
if (color == fruit) // compile error: the compiler doesn't know how to compare different types Color and Fruit
std::cout << "color and fruit are equal\n";
else
std::cout << "color and fruit are not equal\n";
return 0;
}Ten program generuje błąd kompilacji w linii 19, ponieważ wyliczenie o określonym zasięgu nie zostanie przekonwertowane na żaden typ, który można porównać z innym typem. Słowo kluczowe
Na marginesie…
Klasa class (wraz ze słowem kluczowym static ) jest jednym z najbardziej przeciążonych słów kluczowych w języku C++ i może mieć różne znaczenia w zależności od kontekstu. Chociaż wyliczenia o określonym zakresie używają słowa kluczowego class , nie są one uważane za „typ klasy” (który jest zarezerwowany dla struktur, klas i unii).
enum struct działa również w tym kontekście i zachowuje się identycznie jak enum class. Jednakże użycie enum struct nie jest idiomatyczne, więc unikaj jego używania.
Wyliczenia o zakresie definiują własne obszary zakresu
W przeciwieństwie do wyliczeń bez zakresu, które umieszczają swoje moduły wyliczające w tym samym zakresie co samo wyliczenie, wyliczenia o zakresie umieszczają swoje moduły wyliczające tylko w obszarze zasięgu wyliczenia. Innymi słowy, wyliczenia o określonym zakresie działają jak przestrzeń nazw dla ich modułów wyliczających. Ta wbudowana przestrzeń nazw pomaga zmniejszyć zanieczyszczenie globalnej przestrzeni nazw i ryzyko konfliktów nazw, gdy w zakresie globalnym używane są wyliczenia o określonym zakresie.
Aby uzyskać dostęp do modułu wyliczającego o określonym zakresie, robimy to tak, jakby znajdował się on w przestrzeni nazw o tej samej nazwie co wyliczenie o zakresie:
#include <iostream>
int main()
{
enum class Color // "enum class" defines this as a scoped enum rather than an unscoped enum
{
red, // red is considered part of Color's scope region
blue,
};
std::cout << red << '\n'; // compile error: red not defined in this scope region
std::cout << Color::red << '\n'; // compile error: std::cout doesn't know how to print this (will not implicitly convert to int)
Color color { Color::blue }; // okay
return 0;
}Ponieważ wyliczenia o określonym zakresie oferują własne, niejawne przestrzenie nazw dla modułów wyliczających, nie ma potrzeby umieszczania w nim wyliczeń o określonym zakresie inny region zakresu (taki jak przestrzeń nazw), chyba że istnieje ku temu inny istotny powód, ponieważ byłoby to zbędne.
Wyliczenia o określonym zakresie nie są domyślnie konwertowane na liczby całkowite
W przeciwieństwie do modułów wyliczających bez zakresu, moduły wyliczające o zakresie nie będą domyślnie konwertowane na liczby całkowite. W większości przypadków jest to dobre rozwiązanie, ponieważ rzadko ma to sens i pomaga zapobiegać błędom semantycznym, takim jak porównywanie modułów wyliczających z różnych wyliczeń lub wyrażeń, takich jak red + 5.
Pamiętaj, że nadal możesz porównywać moduły wyliczające z tego samego wyliczenia o określonym zakresie (ponieważ są tego samego typu):
#include <iostream>
int main()
{
enum class Color
{
red,
blue,
};
Color shirt { Color::red };
if (shirt == Color::red) // this Color to Color comparison is okay
std::cout << "The shirt is red!\n";
else if (shirt == Color::blue)
std::cout << "The shirt is blue!\n";
return 0;
}Czasami przydatne jest traktowanie modułu wyliczającego o określonym zakresie jako wartości całkowitej. W takich przypadkach można jawnie przekonwertować moduł wyliczający o określonym zakresie na liczbę całkowitą, używając a static_cast. Lepszym wyborem w C++23 jest użycie std::to_underlying() (zdefiniowanego w nagłówku <utility>), które konwertuje moduł wyliczający na wartość typu bazowego wyliczenia.
#include <iostream>
#include <utility> // for std::to_underlying() (C++23)
int main()
{
enum class Color
{
red,
blue,
};
Color color { Color::blue };
std::cout << color << '\n'; // won't work, because there's no implicit conversion to int
std::cout << static_cast<int>(color) << '\n'; // explicit conversion to int, will print 1
std::cout << std::to_underlying(color) << '\n'; // convert to underlying type, will print 1 (C++23)
return 0;
}I odwrotnie, możesz także static_cast liczbę całkowitą do modułu wyliczającego o określonym zakresie, co może być przydatne podczas wprowadzania danych od użytkowników:
#include <iostream>
int main()
{
enum class Pet
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};
std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";
int input{};
std::cin >> input; // input an integer
Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet
return 0;
}Począwszy od C++17, możesz zainicjować listę wyliczenie o ograniczonym zakresie przy użyciu wartości całkowitej bez static_cast (i w przeciwieństwie do wyliczenia bez zakresu, nie trzeba określać podstawy):
// using enum class Pet from prior example
Pet pet { 1 }; // okayNajlepsza praktyka
Preferuj wyliczenia o ograniczonym zakresie zamiast wyliczeń bez zakresu, chyba że istnieje ważny powód, aby zrobić inaczej.
Pomimo korzyści, jakie oferują wyliczenia o zakresie, wyliczenia bez zakresu są nadal powszechnie używane w C++, ponieważ istnieją sytuacje, w których wymagana jest niejawna konwersja na int (robienie dużej ilości rzutowania statycznego staje się denerwujące) i nie potrzebujemy dodatkowej przestrzeni nazw.
Ułatwianie konwersji modułów wyliczających o ograniczonym zakresie na liczby całkowite (zaawansowane)
Wyliczenia o ograniczonym zakresie są świetne, ale brak ukrytej konwersji na liczby całkowite może czasami być problemem. Jeśli musimy często konwertować wyliczenie o zakresie na liczby całkowite (np. w przypadkach, gdy chcemy używać modułów wyliczających o zasięgu jako indeksów tablicy), konieczność używania static_cast za każdym razem, gdy chcemy dokonać konwersji, może znacznie zaśmiecić nasz kod.
Jeśli znajdziesz się w sytuacji, w której przydatne byłoby ułatwienie konwersji modułów wyliczających o zakresie na liczby całkowite, przydatnym sposobem jest przeciążenie jednoargumentowego operator+ w celu wykonania ta konwersja:
#include <iostream>
#include <type_traits> // for std::underlying_type_t
enum class Animals
{
chicken, // 0
dog, // 1
cat, // 2
elephant, // 3
duck, // 4
snake, // 5
maxAnimals,
};
// Overload the unary + operator to convert an enum to the underlying type
// adapted from https://stackoverflow.com/a/42198760, thanks to Pixelchemist for the idea
// In C++23, you can #include <utility> and return std::to_underlying(a) instead
template <typename T>
constexpr auto operator+(T a) noexcept
{
return static_cast<std::underlying_type_t<T>>(a);
}
int main()
{
std::cout << +Animals::elephant << '\n'; // convert Animals::elephant to an integer using unary operator+
return 0;
}Wypisuje:
3
Ta metoda zapobiega niezamierzonym niejawnym konwersjom na typ całkowity, ale zapewnia wygodny sposób jawnego żądania takich konwersji, jeśli jest to konieczne.
using enum instrukcje C++20
Wprowadzone w C++20 instrukcja using enum importuje wszystkie moduły wyliczające z wyliczenia do bieżącego zakresu. W przypadku użycia z typem klasy wyliczeniowej umożliwia nam to dostęp do modułów wyliczających klasy wyliczeniowej bez konieczności poprzedzania każdego z nich nazwą klasy wyliczeniowej.
Może to być przydatne w przypadkach, gdy w przeciwnym razie mielibyśmy wiele identycznych, powtarzających się przedrostków, na przykład w instrukcji switch:
#include <iostream>
#include <string_view>
enum class Color
{
black,
red,
blue,
};
constexpr std::string_view getColor(Color color)
{
using enum Color; // bring all Color enumerators into current scope (C++20)
// We can now access the enumerators of Color without using a Color:: prefix
switch (color)
{
case black: return "black"; // note: black instead of Color::black
case red: return "red";
case blue: return "blue";
default: return "???";
}
}
int main()
{
Color shirt{ Color::blue };
std::cout << "Your shirt is " << getColor(shirt) << '\n';
return 0;
}W powyższym przykładzie Color jest klasą wyliczeniową, więc normalnie uzyskiwalibyśmy dostęp do modułów wyliczających przy użyciu w pełni kwalifikowanej nazwy (np. Color::blue). Jednak w obrębie funkcji getColor() dodaliśmy instrukcję using enum Color;, która umożliwia nam dostęp do tych modułów wyliczających bez Color:: prefiksu.
Dzięki temu nie musimy mieć wielu zbędnych, oczywistych przedrostków wewnątrz instrukcji switch.
Czas quizu
Pytanie nr 1
Zdefiniuj klasę wyliczeniową o nazwie Animal, która zawiera następujące zwierzęta: świnia, kurczak, koza, kot, pies, kaczka. Napisz funkcję o nazwie getAnimalName(), która pobiera parametr Animal i używa instrukcji switch w celu zwrócenia nazwy tego zwierzęcia w postaci std::string_view (lub std::string, jeśli używasz C++ 14). Napisz inną funkcję o nazwie printNumberOfLegs(), która używa instrukcji switch do wydrukowania liczby nóg, po których chodzi każde zwierzę. Upewnij się, że obie funkcje mają domyślną wielkość liter, która wyświetla komunikat o błędzie. Wywołaj printNumberOfLegs() z main() z kotem i kurczakiem. Twoje dane wyjściowe powinny wyglądać następująco:
A cat has 4 legs. A chicken has 2 legs.

