13.4 — Konwersja wyliczenia do i z ciągu znaków

W poprzedniej lekcji (13.3 — Konwersje całkowe modułu wyliczającego bez zakresu), pokazaliśmy taki przykład to:

#include <iostream>

enum Color
{
    black, // 0
    red,   // 1
    blue,  // 2
};

int main()
{
    Color shirt{ blue };

    std::cout << "Your shirt is " << shirt << '\n';

    return 0;
}

Wypisuje:

Your shirt is 2

Ponieważ operator<< nie wie, jak wydrukować Color, kompilator niejawnie przekonwertuje Color na wartość całkowitą i zamiast tego ją wydrukuje.

W większości przypadków drukowanie wyliczenia jako wartości całkowitej (takiej jak 2) nie jest tym, czego chcemy. Zamiast tego zazwyczaj chcemy wydrukować nazwę tego, co reprezentuje moduł wyliczający (np. blue). C++ nie oferuje gotowego sposobu, aby to zrobić, więc sami będziemy musieli znaleźć rozwiązanie. Na szczęście nie jest to bardzo trudne.

Pobieranie nazwy modułu wyliczającego

Typowym sposobem uzyskania nazwy modułu wyliczającego jest napisanie funkcji, która pozwala nam przekazać moduł wyliczający i zwraca jego nazwę w postaci ciągu znaków. Wymaga to jednak pewnego sposobu określenia, który ciąg znaków powinien zostać zwrócony dla danego modułu wyliczającego.

Istnieją na dwa typowe sposoby, aby to zrobić.

W lekcji 8.5 - Podstawy instrukcji Switch zauważyliśmy, że instrukcja switch może przełączać albo wartość całkowitą, albo wartość wyliczeniową. W poniższym przykładzie używamy instrukcji switch, aby wybrać moduł wyliczający i zwrócić odpowiedni literał łańcucha kolorów dla tego modułu wyliczającego:

#include <iostream>
#include <string_view>

enum Color
{
    black,
    red,
    blue,
};

constexpr std::string_view getColorName(Color color)
{
    switch (color)
    {
    case black: return "black";
    case red:   return "red";
    case blue:  return "blue";
    default:    return "???";
    }
}

int main()
{
    constexpr Color shirt{ blue };

    std::cout << "Your shirt is " << getColorName(shirt) << '\n';

    return 0;
}

Wypisuje:

Your shirt is blue

W powyższym przykładzie włączamy color, który przechowuje przekazany przez nas moduł wyliczający. Wewnątrz przełącznika znajduje się etykieta wielkości liter dla każdego modułu wyliczającego Color. Każdy przypadek zwraca nazwę odpowiedniego koloru jako literał ciągu w stylu C. Ten literał ciągu w stylu C jest niejawnie konwertowany na a std::string_view, który jest zwracany do obiektu wywołującego. Mamy także domyślny przypadek, który zwraca "???", na wypadek, gdyby użytkownik przekazał coś, czego się nie spodziewaliśmy.

Przypomnienie

Ponieważ w całym programie istnieją literały łańcuchowe w stylu C, można zwrócić std::string_view który przegląda literał łańcuchowy w stylu C. Kiedy std::string_view zostanie skopiowany z powrotem do wywołującego, przeglądany literał łańcuchowy w stylu C będzie nadal istniał.

Funkcja to constexpr, dzięki czemu możemy użyć nazwy koloru w wyrażeniu stałym.

Powiązana treść

Funkcje Constexpr zostały omówione w lekcji F.1 -- Funkcje Constexpr.

Chociaż pozwala nam to uzyskać nazwę modułu wyliczającego, jeśli chcemy wydrukować tę nazwę na konsoli, musimy zrobić std::cout << getColorName(shirt) nie jest tak miłe jak std::cout << shirt. W następnej lekcji nauczymy std::cout jak wydrukować wyliczenie 13.5 — Wprowadzenie do przeciążania operatorów I/O.

Drugim sposobem rozwiązania programu mapującego wyliczacze na ciągi znaków jest użycie tablicy. Omówiliśmy to w lekcji 17.6 — std::array i wyliczenia.

Wprowadzanie danych przez moduł wyliczający bez zakresu

Przyjrzyjmy się teraz przypadkowi wejściowemu. W poniższym przykładzie definiujemy Pet wyliczenie. Ponieważ Pet jest typem zdefiniowanym przez program, język nie wie, jak wprowadzić Pet za pomocą std::cin:

#include <iostream>

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

int main()
{
    Pet pet { pig };
    std::cin >> pet; // compile error: std::cin doesn't know how to input a Pet

    return 0;
}

Prostym sposobem obejścia tego problemu jest wczytanie liczby całkowitej i użycie static_cast do przekształcenia liczby całkowitej na moduł wyliczający odpowiedniego typu wyliczeniowego:

#include <iostream>
#include <string_view>

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

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 "???";
    }
}

int main()
{
    std::cout << "Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): ";

    int input{};
    std::cin >> input; // input an integer

    if (input < 0 || input > 3)
        std::cout << "You entered an invalid pet\n";
    else
    {
        Pet pet{ static_cast<Pet>(input) }; // static_cast our integer to a Pet
        std::cout << "You entered: " << getPetName(pet) << '\n';
    }

    return 0;
}

Chociaż to działa, jest trochę niewygodne. Należy również pamiętać, że powinniśmy static_cast<Pet>(input) gdy wiemy, że input znajduje się w zakresie modułu wyliczającego.

Uzyskiwanie wyliczenia z ciągu znaków

Zamiast wprowadzać liczbę, byłoby lepiej, gdyby użytkownik mógł wpisać ciąg znaków reprezentujący moduł wyliczający (np. „świnia”) i moglibyśmy przekonwertować ten ciąg na odpowiedni Pet licznik. Jednakże wymaga to od nas rozwiązania kilku wyzwań.

Po pierwsze, nie możemy włączyć ciągu znaków, więc musimy użyć czegoś innego, aby dopasować ciąg znaków przekazany przez użytkownika. Najprostszym podejściem jest tutaj użycie serii instrukcji if.

Po drugie, jaki Pet enumerator powinniśmy zwrócić, jeśli użytkownik przekaże nieprawidłowy ciąg? Jedną z opcji byłoby dodanie modułu wyliczającego reprezentującego „brak/nieprawidłowy” i zwrócenie go. Jednak lepszą opcją jest użycie std::optional tutaj.

Powiązana treść

Omawiamy std::optional w lekcji 12.15 -- std::opcjonalne.

#include <iostream>
#include <optional> // for std::optional
#include <string>
#include <string_view>

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

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)
{
    // We can only switch on an integral value (or enum), not a string
    // so we have to use if-statements here
    if (sv == "cat")   return cat;
    if (sv == "dog")   return dog;
    if (sv == "pig")   return pig;
    if (sv == "whale") return whale;
    
    return {};
}

int main()
{
    std::cout << "Enter a pet: cat, dog, pig, or whale: ";
    std::string s{};
    std::cin >> s;
        
    std::optional<Pet> pet { getPetFromString(s) };

    if (!pet)
        std::cout << "You entered an invalid pet\n";
    else
        std::cout << "You entered: " << getPetName(*pet) << '\n';

    return 0;
}

W powyższym rozwiązaniu używamy serii instrukcji if-else do porównywania ciągów. Jeśli ciąg wejściowy użytkownika pasuje do ciągu wyliczającego, zwracamy odpowiedni moduł wyliczający. Jeżeli żaden z ciągów nie pasuje, zwracamy {}, co oznacza „brak wartości”.

Dla zaawansowanych czytelników

Zauważ, że powyższe rozwiązanie dopasowuje tylko małe litery. Jeśli chcesz dopasować wielkość liter, możesz użyć następującej funkcji, aby przekonwertować dane wejściowe użytkownika na małe litery:

#include <algorithm> // for std::transform
#include <cctype>    // for std::tolower
#include <iterator>  // for std::back_inserter
#include <string>
#include <string_view>

// This function returns a std::string that is the lower-case version of the std::string_view passed in.
// Only 1:1 character mapping can be performed by this function
std::string toASCIILowerCase(std::string_view sv)
{
    std::string lower{};
    std::transform(sv.begin(), sv.end(), std::back_inserter(lower),
        [](char c)
        { 
            return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        });
    return lower;
}

Ta funkcja przechodzi przez każdy znak w std::string_view sv, konwertuje go na małą literę za pomocą std::tolower() (za pomocą lambdy), a następnie dołącza tę małą literę do lower.

Omówimy lambdy w lekcji 20.6 -- Wprowadzenie do lambd (funkcji anonimowych).

Podobnie jak w przypadku wyniku wyjściowego, byłoby lepiej, gdybyśmy mogli po prostu std::cin >> pet. Omówimy to w nadchodzącej lekcji 13.5 — Wprowadzenie do przeciążania operatorów I/O.

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