4.5 Typy wyliczeniowe

Charakterystycznym dla C/C++ typem (a właściwie typami) danych są wyliczenia (zwane też enumeracjami, z angielskiego enumeration).

Formalnie

wyliczenie jest typem zdefiniowanym przez zbiór (zwykle niewielki) nazwanych stałych całkowitych.

Na przykład

       enum dni {pon, wto, sro, czw, pia, sob, nie};
definiuje typ wyliczeniowy o nazwie dni. Zmienne tego typu będą mogły przybierać dokładnie siedem wartości, wyliczonych w nawiasach klamrowych w definicji tego typu (stąd nazwa wyliczenia, typ wyliczeniowy).

Definicja typu wyliczeniowego składa się ze słowa kluczowego enum, nazwy wprowadzanego typu oraz ujętej w nawiasy klamrowe listy nazw symbolicznych wartości elementów wyliczenia. Symbolom można przypisywać wartości liczbowe (patrz niżej), a nazwę typu można opuścić. Jeśli opuszczamy nazwę typu, to zwykle tworzymy od razu zmienne tego anonimowego typu, bo inaczej byłby on bezużyteczny — utworzenie później takich zmiennych byłoby już niemożliwe, bo nie byłoby jak określić ich typu w ich deklaracji/definicji. Na przykład po

       enum {pik, kier, karo, trefl} karta1, karta2;
       ...
       karta1 = kier;
       karta2 = karta1;
       ...
       if (karta2 == trefl) { ... }
mamy dwie zmienne (karta1, karta2) anonimowego typu wyliczeniowego opisującego kolory kart; żadnej innej zmiennej tego typu nie da się już utworzyć, bo typ ten nie ma nazwy. Innym zastosowaniem wyliczeń anonimowych jest wprowadzenie do programu nazwanych stałych, które mogą na przykład służyć do wymiarowania tablic wtedy, gdy definiowanie stałych za pomocą słowa kluczowego const byłoby niewygodne (np. wewnątrz definicji klas; patrz rozdział o listach inicjalizacyjnych ).

Wewnętrznie zmienne typu dni będą reprezentowane za pomocą kolejnych liczb całkowitych poczynając od zera: tak więc symbol pon odpowiadać będzie liczbie 0, a np. symbol sob liczbie 5.

Elementom wyliczenia można nadać odpowiadające im wartości liczbowe „ręcznie”,

       enum dni {pon, wto=0, sro=0, czw=0, pia=0, sob, nie};
przy czym obowiązują następujące zasady:

Przypisane elementom wyliczenia wartości nie tylko nie muszą być różne dla różnych elementów wyliczenia, ale nie muszą też być wartościami kolejnymi; mogą występować „dziury”. Na przykład definicja dni mogłaby wyglądać tak

       enum dni {pon, wto=0, sro=0, czw=0, pia=0, sob, nie=3};
Teraz elementowi sob odpowiada w dalszym ciągu wartość 1, ale niedzieli (nie) odpowiada teraz 3; wartości 2 nie odpowiada zaś teraz żaden element wyliczenia.

W nowym standardzie (C++11), możemy jawnie określić typ stałych wyliczeniowych (musi to być typ całkowitoliczbowy). Robimy to poprzez podanie nazwy typu po nazwie wyliczenia i dwukropku:

       enum kolor : unsigned {pik, kier, karo, trefl};
Według nowego standardu, jeśli nie określimy typu stałych wyliczniowych, to będzie nim int.

Przyjrzyjmy się następnemu przykładowi. Definiujemy w nim () wyliczenie dni. Definicja ta jest globalna, bo jest umieszczona poza wszystkimi funkcjami i klasami (klas tu zresztą w ogóle nie ma). Jest to konieczne, by definicja typu dni była widoczna zarówno w funkcji main, jak i w funkcji info. Następnie definiujemy funkcję info, której parametr jest właśnie typu dni. W programie głównym main wywołujemy tę funkcję. Jako argument podajemy zmienną o wartości typu dni. W linii  jest to pon — literał jednego z elementów wyliczenia dni, a w liniach  jest to zmienna dzien zadeklarowana jako zmienna typu dni. Jak widzimy, nazwę dni traktujemy tak jak nazwę innych typów (np.  int czy double): deklarując zmienną typu dni (), podajemy najpierw nazwę typu, potem nazwę zmiennej i inicjujemy ją jedną z dozwolonych wartości (w tym przypadku wartością sob). W linii  do tej zmiennej przypisujemy inną wartość typu dni, a mianowicie nie.


P14: enums.cpp     Wyliczenia.

      1.  #include <iostream>
      2.  #include <string>
      3.  using namespace std;
      4.  
      5.  enum dni {pon, wto=0, sro=0, czw=0, pia=0, sob, nie};   
      6.  
      7.  void info(dni day) {
      8.      static string  typDnia[]={" powszedni",             
      9.                                "   sobotni", "swiateczny"};
     10.      int stawka = 200*(1 + day);                         
     11.      cout << "Dzien "  << typDnia[day]   << ". "         
     12.           << "Stawka wynosi: " << stawka << " PLN\n";
     13.  }
     14.  
     15.  int main() {
     16.      info(pon);           
     17.  
     18.      dni dzien = sob;     
     19.      info(dzien);         
     20.  
     21.      dzien = nie;         
     22.      info(dzien);         
     23.  }

Wewnątrz funkcji info definiujemy trzyelementową tablicę obiektów typu string (słowem static na razie się nie przejmujmy). Do elementów tej tablicy odwołujemy się w linii . Zauważmy, że w linii tej używamy wartości zmiennej typu dni jako indeksów tablicy: jest to dozwolone, gdyż nastąpi automatyczna konwersja (promocja) tych wartości do typu int zgodnie z wartościami całkowitymi przypisanymi poszczególnym elementom wyliczenia. W naszym przypadku mogą to być wyłącznie wartości 0, 1 i 2, co akurat odpowiada dozwolonym wartościom indeksu tablicy typDnia. Podobnie, w wierszu , widzimy dodawanie ' 1 + day'. Ponieważ jeden argument jest typu int, a drugi typu dni, nastąpi również promocja wartości zmiennej day do wartości typu int. Co jest jednak ważne, to fakt, że nie będzie konwersji w przeciwną stronę: od int do dni. Tak więc niemożliwe byłoby wywołanie funkcji info z argumentem innego typu niż typ dni; na przykład, wywołanie ' int k = 1; info(k);' spowodowałoby błąd kompilacji, mimo że jedynka odpowiada jednej z dozwolonych wartości dla zmiennej typu dni. Ta cecha jest bardzo pożyteczną cechą wyliczeń: użycie typu wyliczeniowego zapewnia, że funkcja info zostanie zawsze wywołana z legalnym argumentem; musi on bowiem być typu dni a więc na pewno odpowiadać którejś z jedynych dopuszczalnych wartości całkowitych: w naszym przypadku 0, 1 lub 2. Dzięki temu nie musimy już sprawdzać, czy indeks w linii  nie wykracza poza zakres — użytkownik funkcji info zostanie zmuszony do świadomego wyboru argumentu, który na pewno nie spowoduje błędu.

Ponieważ nie ma konwersji od typu int do typu dni, zmiennym zadeklarowanym jako zmienne typu dni można przypisywać wartości wyłącznie tego typu. Choć symbolowi sob odpowiada wartość 1, to

       dni day = 1; // ZLE
byłoby nielegalne; należy użyć
       dni day = sob;

Czytelnik może sprawdzić, że wydruk z powyższego programu wygląda następująco:

    Dzien  powszedni. Stawka wynosi: 200 PLN
    Dzien    sobotni. Stawka wynosi: 400 PLN
    Dzien swiateczny. Stawka wynosi: 600 PLN

Kontrola legalności argumentów wysyłanych do funkcji to bardzo częste zastosowanie typów wyliczeniowych.

Nowy standard C++11 wprowadza dodatkowo inny sposób na definiowanie typów wyliczeniowych — powiemy o tym po wprowadzeniu pojęcia klas.

T.R. Werner, 25 lutego 2017; 22:31