13.6 -- Wyliczenia o ograniczonym zakresie (klasy wyliczeniowe)

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 }; // okay

Najlepsza 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.

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
415 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze