W poprzedniej lekcji (13.2 — Wyliczenia bez zakresu), wspomnieliśmy, że moduły wyliczające są stałymi symbolicznymi. Nie powiedzieliśmy wtedy, że te moduły wyliczające mają wartości typu całkowitego.
Jest to podobne do przypadku znaków (4.11 -- Znaki). Rozważmy:
char ch { 'A' };Znak jest tak naprawdę 1-bajtową wartością całkowitą, a znak 'A' jest konwertowany na wartość całkowitą (w tym przypadku 65) i zapisywany.
Kiedy definiujemy wyliczenie, każdy moduł wyliczający jest automatycznie kojarzony z wartością całkowitą na podstawie jego pozycji na liście modułów wyliczających. Domyślnie pierwszy wyliczacz otrzymuje wartość całkowitą 0, a każdy kolejny wyliczacz ma wartość o jeden większą od poprzedniego:
enum Color
{
black, // 0
red, // 1
blue, // 2
green, // 3
white, // 4
cyan, // 5
yellow, // 6
magenta, // 7
};
int main()
{
Color shirt{ blue }; // shirt actually stores integral value 2
return 0;
}Możliwe jest jawne zdefiniowanie wartości wyliczaczy. Te wartości całkowite mogą być dodatnie lub ujemne i mogą mieć tę samą wartość, co inne moduły wyliczające. Niezdefiniowanym wyliczaczom przypisuje się wartość o jeden większą od poprzedniego wyliczacza.
enum Animal
{
cat = -3, // values can be negative
dog, // -2
pig, // -1
horse = 5,
giraffe = 5, // shares same value as horse
chicken, // 6
};Uwaga: w tym przypadku horse i giraffe nadano tę samą wartość. Kiedy tak się dzieje, liczniki stają się nierozróżnialne - zasadniczo horse i giraffe są wymienne. Chociaż C++ na to pozwala, generalnie należy unikać przypisywania tej samej wartości dwóm modułom wyliczającym w tym samym wyliczeniu.
W większości przypadków wartości domyślne dla modułów wyliczających będą dokładnie takie, jakie chcesz, więc nie podawaj własnych wartości, jeśli nie masz ku temu konkretnego powodu.
Najlepsza praktyka
Unikaj przypisywania jawnych wartości do modułów wyliczających, chyba że masz ku temu ważny powód więc.
Wartość inicjująca wyliczenie
Jeśli wyliczenie jest inicjowane zerem (co ma miejsce, gdy używamy inicjalizacji wartości), wyliczeniu zostanie nadana wartość 0, nawet jeśli nie ma odpowiedniego modułu wyliczającego z tą wartością.
#include <iostream>
enum Animal
{
cat = -3, // -3
dog, // -2
pig, // -1
// note: no enumerator with value 0 in this list
horse = 5, // 5
giraffe = 5, // 5
chicken, // 6
};
int main()
{
Animal a {}; // value-initialization zero-initializes a to value 0
std::cout << a; // prints 0
return 0;
}Ma to dwie konsekwencje semantyczne:
- Jeśli istnieje moduł wyliczający przy wartości 0, inicjalizacja wartości domyślnie powoduje wyliczenie zgodnie ze znaczeniem tego modułu wyliczającego. Na przykład, korzystając z poprzedniego
enum Colorprzykładu, zainicjalizowana wartośćColorbędzie domyślnieblack). Z tego powodu dobrym pomysłem jest rozważenie ustawienia modułu wyliczającego o wartości 0 jako tego, który reprezentuje najlepsze domyślne znaczenie wyliczenia.
Coś takiego może powodować problemy:
enum UniverseResult
{
destroyUniverse, // default value (0)
saveUniverse
};- Jeśli nie ma modułu wyliczającego o wartości 0, inicjowanie wartości ułatwia utworzenie semantycznie nieprawidłowego wyliczenia. W takich przypadkach zalecamy dodanie „nieprawidłowego” lub „nieznanego” modułu wyliczającego o wartości 0, aby mieć dokumentację dotyczącą znaczenia tego stanu oraz nazwę tego stanu, którą można jawnie obsłużyć.
enum Winner
{
winnerUnknown, // default value (0)
player1,
player2,
};
// somewhere later in your code
if (w == winnerUnknown) // handle case appropriatelyNajlepsza praktyka
Ustaw moduł wyliczający reprezentujący 0 jako ten, który jest najlepszym domyślnym znaczeniem dla Twojego wyliczenia. Jeśli nie istnieje dobre domyślne znaczenie, rozważ dodanie „nieprawidłowego” lub „nieznanego” modułu wyliczającego o wartości 0, tak aby ten stan był jawnie udokumentowany i mógł być jawnie obsługiwany tam, gdzie to konieczne.
Wyliczenia bez zakresu zostaną niejawnie przekonwertowane na wartości całkowite
Nawet jeśli wyliczenia przechowują wartości całkowite, nie są uważane za typ całkowity (są typem złożonym). Jednak wyliczenie bez zakresu zostanie niejawnie przekonwertowane na wartość całkowitą. Ponieważ moduły wyliczające są stałymi czasowymi kompilacji, jest to konwersja constexpr (omówimy ją w lekcji 10.4 — Konwersje zawężające, inicjowanie list i inicjatory constexpr).
Rozważ następujący program:
#include <iostream>
enum Color
{
black, // assigned 0
red, // assigned 1
blue, // assigned 2
green, // assigned 3
white, // assigned 4
cyan, // assigned 5
yellow, // assigned 6
magenta, // assigned 7
};
int main()
{
Color shirt{ blue };
std::cout << "Your shirt is " << shirt << '\n'; // what does this do?
return 0;
}Ponieważ typy wyliczeniowe zawierają wartości całkowite, jak można się spodziewać, wyświetli się następujący komunikat:
Your shirt is 2
Gdy typ wyliczeniowy jest używany w wywołaniu funkcji lub z operatorem, kompilator najpierw spróbuje znaleźć funkcję lub operator pasujący do wyliczanego typu. Na przykład, gdy kompilator próbuje skompiluj std::cout << shirt, kompilator najpierw sprawdzi, czy operator<< wie jak wydrukować obiekt typu Color (ponieważ shirt jest typu Color) do std::cout. Tak nie jest.
Ponieważ kompilator nie może znaleźć dopasowania, następnie sprawdzi, czy operator<< wie jak wydrukować obiekt typu całkowitego, na który konwertowane jest wyliczenie bez zakresu. Ponieważ tak się dzieje, wartość w shirt jest konwertowana na wartość całkowitą i drukowana jako wartość całkowita 2.
Powiązana treść
Na lekcji pokazujemy, jak zamienić wyliczenie na ciąg znaków 13.4 -- Konwersja wyliczenia na i z ciągu znaków.
Na lekcji std::cout nauczymy<<<M7>>>jak wydrukować moduł wyliczający 13.5 — Wprowadzenie do przeciążania operatorów I/O.
Wielkość wyliczenia i typ bazowy (podstawa)
Enumeratory mają wartości typu całkowitego. Ale jaki typ całkowy? Konkretny typ całkowity używany do reprezentowania wartości modułów wyliczających nazywany jest typem podstawowym wyliczenia (lub base).
W przypadku wyliczeń bez zakresu standard C++ nie określa, który konkretny typ całkowity powinien być używany jako typ podstawowy, więc wybór jest definiowany przez implementację. Większość kompilatorów użyje int jako typu podstawowego (co oznacza, że wyliczenie bez zakresu będzie miało ten sam rozmiar co int), chyba że do przechowywania wartości modułu wyliczającego wymagany jest większy typ. Nie należy jednak zakładać, że będzie to prawdą w przypadku każdego kompilatora lub platformy.
Możliwe jest jawne określenie typu bazowego dla wyliczenia. Typ podstawowy musi być typem całkowitym. Na przykład, jeśli pracujesz w kontekście wrażliwym na przepustowość (np. wysyłasz dane przez sieć), możesz chcieć określić mniejszy typ wyliczenia:
#include <cstdint> // for std::int8_t
#include <iostream>
// Use an 8-bit integer as the enum underlying type
enum Color : std::int8_t
{
black,
red,
blue,
};
int main()
{
Color c{ black };
std::cout << sizeof(c) << '\n'; // prints 1 (byte)
return 0;
}Najlepsza praktyka
Określ podstawowy typ wyliczenia tylko wtedy, gdy jest to konieczne.
Ostrzeżenie
Ponieważ std::int8_t i std::uint8_t są zwykle aliasami typów dla typów znaków, użycie jednego z tych typów jako podstawy wyliczenia najprawdopodobniej spowoduje, że moduły wyliczające będą drukowane jako wartości char, a nie int wartości.
Konwersja liczby całkowitej na moduł wyliczający bez zakresu
Chociaż kompilator niejawnie przekonwertuje wyliczenie bez zakresu na liczbę całkowitą, nie niejawnie przekonwertuje liczbę całkowitą na wyliczenie bez zakresu. Poniższe spowodują błąd kompilatora:
enum Pet // no specified base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};
int main()
{
Pet pet { 2 }; // compile error: integer value 2 won't implicitly convert to a Pet
pet = 3; // compile error: integer value 3 won't implicitly convert to a Pet
return 0;
}Są dwa sposoby obejścia tego problemu.
Po pierwsze, możesz jawnie przekonwertować liczbę całkowitą na moduł wyliczający o niezakresowym użyciu static_cast:
enum Pet // no specified base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};
int main()
{
Pet pet { static_cast<Pet>(2) }; // convert integer 2 to a Pet
pet = static_cast<Pet>(3); // our pig evolved into a whale!
return 0;
}Przykład zobaczymy w lekcji 13.4 -- Konwersja wyliczenia na i z ciągu znaków gdzie z tego skorzystamy.
Możesz bezpiecznie wykonać static_cast dowolną wartość całkowitą reprezentowaną przez moduł wyliczający wyliczenie docelowe. Ponieważ nasze Pet wyliczenie zawiera moduły wyliczające z wartościami 0, 1, 2, I 3, wartości całkowite static_casting 0, 1, 2, I 3 do Pet są prawidłowe.
Bezpieczne jest także static_castowanie dowolnej wartości całkowitej znajdującej się w zakresie typu bazowego wyliczenia docelowego, nawet jeśli nie ma żadnych modułów wyliczających reprezentujących tę wartość. Statyczne rzutowanie wartości poza zakresem typu bazowego spowoduje niezdefiniowane zachowanie.
Dla zaawansowanych czytelników
Jeśli wyliczenie ma jawnie zdefiniowany typ podstawowy, zakres wyliczenia jest identyczny z zakresem typu podstawowego.
Jeśli wyliczenie nie ma jawnego typu bazowego, sytuacja jest nieco bardziej skomplikowana. W tym przypadku kompilator wybiera typ bazowy i może wybrać dowolny typ ze znakiem lub bez znaku, o ile wartość wszystkich modułów wyliczających mieści się w tym typie. Biorąc to pod uwagę, bezpieczne jest rzucanie wartości całkowitych static_cast tylko w zakresie najmniejszej liczby bitów, które mogą pomieścić wartość wszystkich modułów wyliczających.
Zróbmy dwa przykłady, aby to zilustrować:
- W przypadku modułów wyliczających, które mają wartości 2, 9 i 12, te wyliczacze mogą minimalnie zmieścić się w 4-bitowym typie całkowitym bez znaku z zakresem od 0 do 15. Dlatego też jest to static_cast można bezpiecznie rzucać tylko wartości całkowite od 0 do 15 na ten typ wyliczeniowy.
- W przypadku modułów wyliczających, które mają wartości -28, 2 i 6, te moduły wyliczające mogą minimalnie zmieścić się w 6-bitowym typie całkowitym ze znakiem z zakresu od -32 do 31. Dlatego bezpieczne jest przesyłanie wartości całkowitych od -32 do 31 do tego wyliczeniowego typu static_cast.
Po drugie, począwszy od C++ 17, jeśli wyliczenie bez zakresu ma jawnie określoną podstawę, wówczas kompilator umożliwi zainicjowanie listy wyliczenia bez zakresu przy użyciu wartości całkowitej:
enum Pet: int // we've specified a base
{
cat, // assigned 0
dog, // assigned 1
pig, // assigned 2
whale, // assigned 3
};
int main()
{
Pet pet1 { 2 }; // ok: can brace initialize unscoped enumeration with specified base with integer (C++17)
Pet pet2 (2); // compile error: cannot direct initialize with integer
Pet pet3 = 2; // compile error: cannot copy initialize with integer
pet1 = 3; // compile error: cannot assign with integer
return 0;
}Czas quizu
Pytanie nr 1
Prawda lub fałsz. Licznikami mogą być:
- Przy wartości całkowitej
- Przy braku jawnej wartości
- Przy wartości zmiennoprzecinkowej
- Przy wartości ujemnej
- Przy nieunikalnej wartości wartość
- Biorąc pod uwagę wartość poprzedniego modułu wyliczającego (np. magenta = czerwony)
- Biorąc pod uwagę wartość inną niż constexpr

