Jednym z większych problemów z dokumentacją tablic jest to, że indeksy całkowite nie dostarczają programiście żadnych informacji o znaczeniu indeksu.
Rozważmy tablicę zawierającą 5 wyników testu:
#include <vector>
int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };
testScores[2] = 76; // who does this represent?
}Kim jest uczeń reprezentowany przez testScores[2]? Nie jest to jasne.
Używając modułów wyliczających bez zakresu do indeksowania
W lekcji 16.3 -- std::vector oraz problemu długości bez znaku i indeksu dolnego, spędziliśmy dużo czasu omawiając, w jaki sposób indeks std::vector<T>::operator[] (i innych klas kontenerów C++, które mogą być indeksowane) ma typ size_type, czyli ogólnie alias dla std::size_t. W związku z tym nasze indeksy muszą być typu std::size_t lub typu, który konwertuje na std::size_t.
Ponieważ wyliczenia bez zakresu zostaną niejawnie skonwertowane na a std::size_t, oznacza to, że możemy używać wyliczeń bez zakresu jako indeksów tablicy, aby pomóc udokumentować znaczenie indeksu:
#include <vector>
namespace Students
{
enum Names
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
}
int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };
testScores[Students::stan] = 76; // we are now updating the test score belonging to stan
return 0;
}W ten sposób znacznie jaśniejsze jest, co każdy z elementów tablicy reprezentuje.
Ponieważ moduły wyliczające są domyślnie constexpr, konwersja modułu wyliczającego na typ całkowity bez znaku nie jest uważana za konwersję zawężającą, co pozwala uniknąć problemów z indeksowaniem ze znakiem/bez znaku.
Używanie do indeksowania wyliczenia bez zakresu innego niż constexpr
Podstawowy typ wyliczenia bez zakresu jest zdefiniowany w implementacji (a zatem może być albo znakiem, albo typ całkowity bez znaku). Ponieważ moduły wyliczające są w sposób dorozumiany constexpr, tak długo jak będziemy trzymać się indeksowania z modułami wyliczającymi bez zakresu, nie napotkamy problemów z konwersją znaków.
Jeśli jednak zdefiniujemy zmienną typu wyliczeniowego inną niż constexpr, a następnie spróbujemy zaindeksować nasze std::vector za pomocą tego, możemy otrzymać ostrzeżenia o konwersji znaku na dowolnej platformie, która domyślnie ustawia wyliczenia bez zakresu na znak type:
#include <vector>
namespace Students
{
enum Names
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
}
int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };
Students::Names name { Students::stan }; // non-constexpr
testScores[name] = 76; // may trigger a sign conversion warning if Student::Names defaults to a signed underlying type
return 0;
}W tym konkretnym przypadku moglibyśmy wykonać name constexpr (tak, aby konwersja z typu całkowitego ze znakiem constexpr na std::size_t nie była zawężająca). Jednak to nie zadziała, jeśli nasz inicjator nie jest wyrażeniem stałym.
Alternatywną opcją jest jawne określenie typu bazowego wyliczenia jako unsigned int:
#include <vector>
namespace Students
{
enum Names : unsigned int // explicitly specifies the underlying type is unsigned int
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
}
int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };
Students::Names name { Students::stan }; // non-constexpr
testScores[name] = 76; // not a sign conversion since name is unsigned
return 0;
}W powyższym przykładzie, ponieważ name jest teraz gwarantowane jako unsigned int, można go przekonwertować na std::size_t bez konwersji znaku problemy.
Korzystanie z modułu wyliczającego zliczanie
Zauważ, że na końcu listy modułów wyliczających zdefiniowaliśmy dodatkowy moduł wyliczający o nazwie max_students. Jeśli wszystkie poprzednie moduły wyliczające używają wartości domyślnych (co jest zalecane), ten moduł wyliczający będzie miał wartość domyślną odpowiadającą liczbie poprzednich modułów wyliczających. W powyższym przykładzie max_students ma wartość 5, ponieważ wcześniej zdefiniowanych jest 5 modułów wyliczających. Nieformalnie nazwiemy to licznikiem wyliczającym, ponieważ jego wartość reprezentuje liczbę wcześniej zdefiniowanych modułów wyliczających.
Tego modułu wyliczającego można następnie użyć wszędzie tam, gdzie potrzebujemy zliczenia poprzednich modułów wyliczających. Na przykład:
#include <iostream>
#include <vector>
namespace Students
{
enum Names
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
// add future enumerators here
max_students // 5
};
}
int main()
{
std::vector<int> testScores(Students::max_students); // Create a vector with 5 elements
testScores[Students::stan] = 76; // we are now updating the test score belonging to stan
std::cout << "The class has " << Students::max_students << " students\n";
return 0;
}Używamy max_students w dwóch miejscach: najpierw tworzymy std::vector o długości max_students, więc wektor będzie miał jeden element na ucznia. Używamy również max_students do wydrukowania liczby uczniów.
Ta technika jest również dobra, ponieważ jeśli później dodany zostanie kolejny moduł wyliczający (tuż przed max_students), wówczas max_students automatycznie stanie się o jeden większy, a wszystkie nasze tablice używające max_students zostaną zaktualizowane, aby używać nowej długości bez dalszych modyfikacji.
#include <vector>
#include <iostream>
namespace Students
{
enum Names
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
wendy, // 5 (added)
// add future enumerators here
max_students // now 6
};
}
int main()
{
std::vector<int> testScores(Students::max_students); // will now allocate 6 elements
testScores[Students::stan] = 76; // still works
std::cout << "The class has " << Students::max_students << " students\n";
return 0;
}Stwierdzanie długości tablicy za pomocą liczby moduł wyliczający
Częściej tworzymy tablicę, korzystając z inicjującej listy wartości, z zamiarem indeksowania tej tablicy za pomocą modułów wyliczających. W takich przypadkach przydatne może być stwierdzenie, że rozmiar kontenera jest równy naszemu modułowi wyliczającemu. Jeśli to potwierdzenie zostanie uruchomione, oznacza to, że albo nasza lista modułów wyliczających jest w jakiś sposób niepoprawna, albo udostępniliśmy niewłaściwą liczbę inicjatorów. Może się to łatwo zdarzyć, gdy do wyliczenia zostanie dodany nowy moduł wyliczający, ale do tablicy nie zostanie dodana nowa wartość inicjująca.
Na przykład:
#include <cassert>
#include <iostream>
#include <vector>
enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };
// Ensure the number of test scores is the same as the number of students
assert(std::size(testScores) == max_students);
return 0;
}Wskazówka
Jeśli tablicą jest constexpr, powinieneś zamiast tego static_assert . std::vector nie obsługuje constexpr, ale std::array (i tablice w stylu C) tak.
Omawiamy to szerzej w lekcja 17.3 — Przekazywanie i zwracanie std::array.
Najlepsza praktyka
Użyj static_assert aby upewnić się, że długość tablicy constexpr odpowiada modułowi wyliczającemu count.
Użyj assert aby upewnić się, że długość tablicy niebędącej constexpr odpowiada modułowi wyliczającemu count.
Tablice i wyliczenie klasy
Ponieważ wyliczenia bez zakresu zanieczyszczają przestrzeń nazw, w której są zdefiniowane, za pomocą swoich modułów wyliczających, lepiej jest używać klas wyliczeniowych w przypadkach, gdy wyliczenie nie jest już zawarte w innym obszarze zasięgu (np. przestrzeni nazw lub klasie).
Jednak ponieważ klasy wyliczeniowe nie mają ukrytej konwersji na typy całkowite, pojawia się problem, gdy próbujemy użyć ich wyliczeń jako tablicy indeksy:
#include <iostream>
#include <vector>
enum class StudentNames // now an enum class
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
int main()
{
// compile error: no conversion from StudentNames to std::size_t
std::vector<int> testScores(StudentNames::max_students);
// compile error: no conversion from StudentNames to std::size_t
testScores[StudentNames::stan] = 76;
// compile error: no conversion from StudentNames to any type that operator<< can output
std::cout << "The class has " << StudentNames::max_students << " students\n";
return 0;
}Istnieje kilka sposobów rozwiązania tego problemu. Najwyraźniej możemy static_cast zamienić moduł wyliczający na liczbę całkowitą:
#include <iostream>
#include <vector>
enum class StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
int main()
{
std::vector<int> testScores(static_cast<int>(StudentNames::max_students));
testScores[static_cast<int>(StudentNames::stan)] = 76;
std::cout << "The class has " << static_cast<int>(StudentNames::max_students) << " students\n";
return 0;
}Jednak jest to nie tylko kłopotliwe w pisaniu, ale także znacznie zaśmieca nasz kod.
Lepszą opcją jest użycie funkcji pomocniczej, którą przedstawiliśmy w lekcji 13.6 -- Wyliczenia o ograniczonym zakresie (klasy wyliczeniowe), które pozwalają nam przekonwertuj moduły wyliczające klas wyliczeniowych na wartości całkowite za pomocą jednoargumentowego operator+.
#include <iostream>
#include <type_traits> // for std::underlying_type_t
#include <vector>
enum class StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};
// Overload the unary + operator to convert StudentNames to the underlying type
constexpr auto operator+(StudentNames a) noexcept
{
return static_cast<std::underlying_type_t<StudentNames>>(a);
}
int main()
{
std::vector<int> testScores(+StudentNames::max_students);
testScores[+StudentNames::stan] = 76;
std::cout << "The class has " << +StudentNames::max_students << " students\n";
return 0;
}Jeśli jednak zamierzasz wykonywać wiele konwersji wyliczeniowych na całkowe, prawdopodobnie lepiej będzie po prostu użyć standardowego wyliczenia w przestrzeni nazw (lub klasie).
Czas quizu
Pytanie nr 1
Utwórz wyliczenie zdefiniowane przez program (wewnątrz przestrzeni nazw) zawierające nazwy następujących zwierząt: kurczak, pies, kot, słoń, kaczka i wąż. Zdefiniuj tablicę z elementem dla każdego z tych zwierząt i użyj listy inicjującej, aby zainicjować każdy element w celu przechowywania liczby nóg zwierzęcia. Sprawdź, czy tablica ma poprawną liczbę inicjatorów.
Napisz funkcję main(), która wypisuje liczbę nóg słonia za pomocą modułu wyliczającego.

