C++11 #6: Silne typy wyliczeniowe.

|

Większość programistów języka C++ zna doskonale typy wyliczeniowe (enum), zdefiniowane w standardzie C++98/03. Jest to wygodne, często wykorzystywane narzędzie. Jest to jednak również funkcjonalność, która posiada kilka zauważalnych niedociągnięć, przez co użycie typów wyliczeniowych nie jest tak przyjemne i odporne na błędy, jak mogłoby być.

W standardzie C++11 doczekaliśmy się pod tym względem malutkiej rewolucji – zdefiniowane zostały tzw. silne typy wyliczeniowe (ang. strongly-typed enums).

Silny typ wyliczeniowy jest definiowany parą słów kluczowych enum class, w ten sposób:

enum class Level 
{ 
    First, 
    Second, 
    Third 
}; 

Level myLevel = Level::Second;

Jeśli chodzi o taką deklarację typu, jedyną różnicą pomiędzy „starym” a „nowym” jest słowo kluczowe class. Nowości jest jednak znacznie więcej…

Na czym polegają zmiany ?

Kluczowe są 3 różnice pomiędzy silnym typem wyliczeniowym, a jego poprzednikiem. W przypadku enum class:

  • nie wystąpi niejawna konwersja wartości do typu całkowitego (i dalej, np. do typu zmiennoprzecinkowego)
  • wartości nie zostaną wyeksportowane do zawierającego typ wyliczeniowy zakresu
  • bazowy typ wartości może być jawnie zdefiniowany, ale musi to być jeden z typów całkowitych, ze znakiem lub bez (domyślnie jest to int)

Zobaczmy, jak nowości wyglądają za żywo.

Pierwszy przykład:

int main() 
{ 
    enum class Level 
    { 
        First, 
        Second, 
        Third 
    }; 

    Level level = 2; // błąd: brak konwersji int-->Level 
    int e1 = Level::Second; // błąd: brak konwersji Level-->int 

    return 0; 
}

Powyższy kod obrazuje pierwszy punkt na liście. Nie mamy tutaj w przypadku enum class niejawnych konwersji. Gdybyśmy jednak użyli starego typu wyliczeniowego, linia numer 11. skompiluje się bez problemów – nastąpi konwersja do typu całkowitego.

Dlaczego zatem zajęto się tą kwestią ? Niejawna konwersja może powodować problemy, jeśli nie chcemy, by typ zachowywał się jak liczba całkowita. Przed standardem C++11 było to domyślne zachowanie. Nowe rozwiązanie jest wg mnie bezpieczniejsze i bardziej zrozumiałe.

Spójrzmy na drugi przykład:

int main() 
{ 
    enum class Level 
    { 
        First, 
        Second, 
        Third 
    }; 

    Level e1 = First; // błąd: Co to jest First ? Nie wiadomo... 
    Level e2 = Level::Second; // ok 

    return 0; 
}

W drugim przykładzie mamy zobrazowany drugi punt z listy. W przypadku enum class wartości nie są eksportowane do zawierającego zakresu. Z tego powodu musimy użyć nazwy zdefiniowanego typu jako zakresu.

Co nam daje ta zmiana ? Pozwoli nam uniknąć konfliktu nazw w zakresie zawierającym oraz zwiększy czytelność kodu. Uważam to za bardzo dobrą zmianę.

Wreszcie przykład numer 3:

enum class Value : long 
{ 
    Value1 = 4, 
    Value2 = 7, 
    Value3 = 9999999999 
};

Widzimy tutaj, jak jawnie zadeklarować typ bazowy dla typu wyliczeniowego. Dzięki temu możliwe staje się stwierdzenie, ile dokładnie miejsca w pamięci zajmie wyliczenie.

Drugą ważną zaletą jest możliwość skorzystania z tzw. deklaracji wyprzedzającej (ang. forward declaration), czyli:

enum class Value : long; 

void function(Value* a_value); 

enum class Value : long 
{ 
    Value1 = 4, 
    Value2 = 7, 
    Value3 = 9999999999 
};

 

Powyższy kod jest w C++11 jak najbardziej poprawny.

Podsumowanie.

Silny typ wyliczeniowy to taki enum na sterydach. Zmiany są jednak przemyślane i wprowadzają nową jakość oraz lepszą odporność na błędy w przypadku użycia wyliczeń w naszym kodzie.

Można powiedzieć, że nieprzypadkowo silne typy wyliczeniowe definiuje się parą słów kluczowych enum class. Łączą one w sobie własności poprzednika (umożliwiają definiowanie nazwanych wartości) oraz właściwości klas (brak konwersji, pola chronione zakresem).

Nowy typ wyliczeniowy jest już w tej chwili stosowany w wielu miejscach w bibliotece standardowej języka. Oczywiście z powodu kompatybilności wstecznej, nadal możemy korzystać ze starszego rozwiązania. Czy ma to sens ? Moim zdaniem nie ma żadnego 🙂

Kontynuuj poznawanie C++11: C++11 #7: Przeciążanie funkcji wirtualnych: specyfikator override.

6 komentarzy do “C++11 #6: Silne typy wyliczeniowe.”

  1. Może „strongly-typed enums” powinno się tłumaczyć „silnie typowany typ wyliczeniowy”? Nie wiem, ta wersja jest mniej zgrabna.

    Odpowiedz
    • To jest generalnie temat rzeka – trudno tłumaczy się taki techniczny angielski. Tłumaczenie czegoś jeden do jednego prawie zawsze jest złym pomysłem – wygląda pokracznie i dość niezrozumiale. Dlatego zawsze chętnie przyjmę pomysły odnośnie nazewnictwa takich konstrukcji w języku polskim 🙂

  2. Tłumaczenie ,jak to zostało określone 'jeden do jednego’, językoznawcy nazywają kalką. Bardzo jej nie lubią. Zdecydowanie polubili by silny typ wyliczeniowy. Zwłaszcza że w literaturze jest już termin silne typowanie odnoszący się do języków programowania.

    Odpowiedz
    • Tak, wspomniałem też w artykule o możliwości użycia deklaracji wyprzedzającej — w takim wypadku kompilator musi znać konkretny typ wyliczenia.

      Jeśli jednak nie potrzebujemy, najlepiej nie deklarować jawnie typu bazowego wyliczenia ani nie przypisywać wartości poszczególnym opcjom.

Dodaj komentarz