17.6 — std::array i enumerations

W lekcji >16.9 -- Indeksowanie i długość tablicy przy użyciu modułów wyliczających, omówiliśmy tablice i wyliczenia.

Teraz, gdy już skończyliśmy mamy constexpr std::array w naszym zestawie narzędzi, będziemy kontynuować tę dyskusję i pokażemy kilka dodatkowych sztuczek.

Korzystając ze statycznego potwierdzenia, aby zapewnić odpowiednią liczbę inicjatorów tablicy

Podczas inicjalizacji constexpr std::array za pomocą CTAD, kompilator wydedukuje, jak długa powinna być tablica na podstawie liczby inicjatorów. Jeśli podano mniej inicjatorów niż powinno, tablica będzie krótsza niż oczekiwano, a indeksowanie jej może prowadzić do niezdefiniowanego zachowania.

Na przykład:

#include <array>
#include <iostream>

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    constexpr std::array testScores { 78, 94, 66, 77 }; // oops, only 4 values

    std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n'; // undefined behavior due to invalid index

    return 0;
}

Za każdym razem, gdy liczba inicjatorów w constexpr std::array może zostać w rozsądny sposób sprawdzona, możesz to zrobić za pomocą statycznej asercji:

#include <array>
#include <iostream>

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    constexpr std::array testScores { 78, 94, 66, 77 };

    // Ensure the number of test scores is the same as the number of students
    static_assert(std::size(testScores) == max_students); // compile error: static_assert condition failed

    std::cout << "Cartman got a score of " << testScores[StudentNames::cartman] << '\n';

    return 0;
}

W ten sposób, jeśli później dodasz nowy moduł wyliczający, ale zapomnisz dodać odpowiedni inicjator do testScores, program to zrobi nie udało się skompilować.

Możesz także użyć asercji statycznej, aby upewnić się, że dwa różne constexpr std::array mają tę samą długość.

Wykorzystując tablice constexpr w celu lepszego wprowadzania i wyprowadzania wyliczeń

W lekcji 13.5 — Wprowadzenie do przeciążania operatorów I/O, omówiliśmy kilka sposobów wprowadzania i wysyłania nazw modułów wyliczających. Aby pomóc w tym zadaniu, udostępniliśmy funkcje pomocnicze, które konwertują moduł wyliczający na ciąg znaków i odwrotnie. Każda z tych funkcji miała swój własny (zduplikowany) zestaw literałów łańcuchowych i musieliśmy specjalnie zakodować logikę, aby sprawdzić każdą z nich:

constexpr std::string_view getPetName(Pet pet)
{
    switch (pet)
    {
    case cat:   return "cat";
    case dog:   return "dog";
    case pig:   return "pig";
    case whale: return "whale";
    default:    return "???";
    }
}

constexpr std::optional<Pet> getPetFromString(std::string_view sv)
{
    if (sv == "cat")   return cat;
    if (sv == "dog")   return dog;
    if (sv == "pig")   return pig;
    if (sv == "whale") return whale;

    return {};
}

Oznacza to, że gdybyśmy mieli dodać nowy moduł wyliczający, musielibyśmy pamiętać o zaktualizowaniu tych funkcji.

Ulepszmy trochę te funkcje. W przypadkach, gdy wartość naszych modułów wyliczających zaczyna się od 0 i przebiega sekwencyjnie (co jest prawdą w przypadku większości wyliczeń), możemy użyć tablicy do przechowywania nazwy każdego modułu wyliczającego.

To pozwala nam zrobić dwie rzeczy:

  1. Indeksować tablicę przy użyciu wartości modułu wyliczającego, aby uzyskać nazwę tego modułu wyliczającego.
  2. Użyj pętli, aby iterować po wszystkich nazwy i móc powiązać nazwę z modułem wyliczającym na podstawie indeksu.
#include <array>
#include <iostream>
#include <string>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // use sv suffix so std::array will infer type as std::string_view
    using namespace std::string_view_literals; // for sv suffix
    constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };

    // Make sure we've defined strings for all our colors
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    // We can index the array using the enumerator to get the name of the enumerator
    return Color::colorName[static_cast<std::size_t>(color)];
}

// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

// Teach operator>> how to input a Color by name
// We pass color by non-const reference so we can have the function modify its value
std::istream& operator>> (std::istream& in, Color::Type& color)
{
    std::string input {};
    std::getline(in >> std::ws, input);

    // Iterate through the list of names to see if we can find a matching name
    for (std::size_t index=0; index < Color::colorName.size(); ++index)
    {
        if (input == Color::colorName[index])
        {
            // If we found a matching name, we can get the enumerator value based on its index
            color = static_cast<Color::Type>(index);
            return in;
        }
    }

    // We didn't find a match, so input must have been invalid
    // so we will set input stream to fail state
    in.setstate(std::ios_base::failbit);

    // On an extraction failure, operator>> zero-initializes fundamental types
    // Uncomment the following line to make this operator do the same thing
    // color = {};
    return in;
}

int main()
{
    auto shirt{ Color::blue };
    std::cout << "Your shirt is " << shirt << '\n';

    std::cout << "Enter a new color: ";
    std::cin >> shirt;
    if (!std::cin)
        std::cout << "Invalid\n";
    else
        std::cout << "Your shirt is now " << shirt << '\n';

    return 0;
}

Wypisuje:

Your shirt is blue
Enter a new color: red
Your shirt is now red

Pętle for i wyliczenia oparte na zakresach

Czasami spotykamy się z sytuacjami, w których przydatne byłoby iterowanie po licznikach wyliczenia. Chociaż możemy to zrobić za pomocą pętli for z indeksem całkowitym, prawdopodobnie będzie to wymagało dużej ilości statycznego rzutowania indeksu całkowitego na nasz typ wyliczeniowy.

#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // use sv suffix so std::array will infer type as std::string_view
    using namespace std::string_view_literals; // for sv suffix
    constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };

    // Make sure we've defined strings for all our colors
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    // Use a for loop to iterate through all our colors
    for (int i=0; i < Color::max_colors; ++i )
        std::cout << static_cast<Color::Type>(i) << '\n';

    return 0;
}

Niestety, pętle for oparte na zakresach nie pozwalają na iterację po modułach wyliczających wyliczenie:

#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // use sv suffix so std::array will infer type as std::string_view
    using namespace std::string_view_literals; // for sv suffix
    constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };

    // Make sure we've defined strings for all our colors
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    for (auto c: Color::Type) // compile error: can't traverse enumeration
        std::cout << c < '\n';

    return 0;
}

Istnieje wiele kreatywnych rozwiązań tego problemu. Ponieważ w tablicy możemy zastosować pętlę for opartą na zakresach, jednym z najprostszych rozwiązań jest utworzenie constexpr std::array zawierającego każdy z naszych modułów wyliczających, a następnie iteracja po nim. Ta metoda działa tylko wtedy, gdy moduły wyliczające mają unikalne wartości.

#include <array>
#include <iostream>
#include <string_view>

namespace Color
{
    enum Type
    {
        black,     // 0
        red,       // 1
        blue,      // 2
        max_colors // 3
    };

    using namespace std::string_view_literals; // for sv suffix
    constexpr std::array colorName { "black"sv, "red"sv, "blue"sv };
    static_assert(std::size(colorName) == max_colors);

    constexpr std::array types { black, red, blue }; // A std::array containing all our enumerators
    static_assert(std::size(types) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// Teach operator<< how to print a Color
// std::ostream is the type of std::cout
// The return type and parameter type are references (to prevent copies from being made)!
std::ostream& operator<<(std::ostream& out, Color::Type color)
{
    return out << getColorName(color);
}

int main()
{
    for (auto c: Color::types) // ok: we can do a range-based for on a std::array
        std::cout << c << '\n';

    return 0;
}

W powyższym przykładzie, ponieważ typ elementu Color::types Jest Color::Type, zmienna c zostanie wydedukowana jako a Color::Type, czyli dokładnie to, czego chcemy!

Wypisuje:

black
red
blue

Czas quizu

Zdefiniuj przestrzeń nazw o nazwie Animal. Wewnątrz niego zdefiniuj wyliczenie zawierające następujące zwierzęta: kurczak, pies, kot, słoń, kaczka i wąż. Utwórz także strukturę o nazwie Data , w której będzie przechowywana nazwa każdego zwierzęcia, liczba jego nóg i wydawany przez nie dźwięk. Utwórz std::array Dane i wypełnij element Dane dla każdego zwierzęcia.

Poproś użytkownika o wprowadzenie imienia zwierzęcia. Jeśli imię nie pasuje do imienia któregoś z naszych zwierząt, powiedz mu o tym. W przeciwnym razie wydrukuj dane tego zwierzęcia. Następnie wydrukuj dane wszystkich pozostałych zwierząt, które nie pasowały do ​​wprowadzonych danych.

Na przykład:

Enter an animal: dog
A dog has 4 legs and says woof.

Here is the data for the rest of the animals:
A chicken has 2 legs and says cluck.
A cat has 4 legs and says meow.
A elephant has 4 legs and says pawoo.
A duck has 2 legs and says quack.
A snake has 0 legs and says hissss.
Enter an animal: frog
That animal couldn't be found.

Here is the data for the rest of the animals:
A chicken has 2 legs and says cluck.
A dog has 4 legs and says woof.
A cat has 4 legs and says meow.
A elephant has 4 legs and says pawoo.
A duck has 2 legs and says quack.
A snake has 0 legs and says hissss.

Pytanie nr 1

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:  
92 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze