16.8 — Pętle for oparte na zakresach (dla każdej)

W lekcji 16.6 -- Tablice i pętle, pokazaliśmy przykłady, w których użyliśmy pętli for do iteracji po każdym elemencie tablicy, używając zmiennej pętli jako indeksu. Oto kolejny przykład:

#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    std::size_t length { fibonacci.size() };
    for (std::size_t index { 0 }; index < length; ++index)
       std::cout << fibonacci[index] << ' ';

    std::cout << '\n';

    return 0;
}

Chociaż pętle for zapewniają wygodny i elastyczny sposób iteracji po tablicy, łatwo je zepsuć, są podatne na błędy o jeden i mogą powodować problemy ze znakami indeksowania tablicy (omówione w lekcji 16.7 — Tablice, pętle i rozwiązania w zakresie wyzwań związanych ze znakami).

Ponieważ przechodzenie (do przodu) przez tablicę jest tak powszechne, C++ obsługuje inny typ pętli for, zwany a pętla for oparta na zakresie (czasami nazywany także a dla każdej pętli), który umożliwia przechodzenie przez kontener bez konieczności wykonywania jawnego indeksowania. Pętle for oparte na zakresach są prostsze, bezpieczniejsze i działają ze wszystkimi popularnymi typami tablic w C++ (w tym std::vector, std::array i tablicami w stylu C).

Pętle for oparte na zakresach

Klasa pętle for oparta na zakresie ma składnię wyglądającą następująco:

for (element_declaration : array_object)
   statement;

Gdy napotkana zostanie pętla for oparta na zakresie, pętla wykona iterację przez każdą element w array_object. Dla każdej iteracji wartość bieżącego elementu tablicy zostanie przypisana do zmiennej zadeklarowanej w element_declaration, a następnie statement wykona.

Aby uzyskać najlepsze wyniki, element_declaration powinno być tego samego typu co elementy tablicy, w przeciwnym razie nastąpi konwersja typów.

Oto prosty przykład wykorzystujący pętle for do wydrukowania wszystkich elementów tablicy o nazwie fibonacci:

#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (int num : fibonacci) // iterate over array fibonacci and copy each value into `num`
       std::cout << num << ' '; // print the current value of `num`

    std::cout << '\n';

    return 0;
}

Wypisuje:

0 1 1 2 3 5 8 13 21 34 55 89

Zauważ, że ten przykład nie wymaga od nas użycia długości tablicy ani indeksowania tablicy!

Przyjrzyjmy się bliżej, jak to działa. Ta pętla for oparta na zakresie zostanie wykonana przez wszystkie elementy fibonacci. Dla pierwszej iteracji zmiennej num przypisuje się wartość pierwszego elementu (0). Następnie program wykonuje powiązaną instrukcję, która wypisuje na konsoli wartość num (0. W drugiej iteracji num przypisuje się wartość drugiego elementu (1). Powiązana instrukcja jest wykonywana ponownie, co powoduje wypisanie 1. Pętla for oparta na zakresie kontynuuje iterację po kolei przez każdy element tablicy, wykonując dla każdego z nich powiązaną instrukcję, dopóki w tablicy nie pozostaną żadne elementy do iteracji. W tym momencie pętla kończy się, a program kontynuuje wykonywanie (wypisuje znak nowej linii, a następnie powraca 0 do systemu operacyjnego).

Kluczowa informacja

Zadeklarowany element (num w poprzednim przykładzie) nie jest indeksem tablicy. Zamiast tego przypisuje się mu wartość elementu tablicy, po którym iteruje się.

Ponieważ num przypisuje się wartość elementu tablicy, oznacza to jednak, że element tablicy jest kopiowany (co może być kosztowne w przypadku niektórych typów).

Najlepsza praktyka

Przy przechodzeniu przez kontenery preferuj pętle oparte na zakresie zamiast zwykłych pętli for.

Oparte na zakresie dla pętli i pustych kontenerów

W przypadkach, gdy kontener jest trawersed nie zawiera żadnych elementów, treść pętli for opartej na zakresie po prostu nie zostanie wykonana:

#include <iostream>
#include <vector>

int main()
{
    std::vector empty { };

    for (int num : empty)
       std::cout << "Hi mom!\n";

    return 0;
}

Powyższy przykład niczego nie wyświetla. Przepraszam mamo!

Pętle for oparte na zakresie i dedukcja typu przy użyciu auto słowo kluczowe

Ponieważ element_declaration powinny mieć ten sam typ co elementy tablicy (aby zapobiec konwersji typów). Jest to idealny przypadek, w którym można użyć słowa kluczowego auto i pozwolić kompilatorowi wydedukować za nas typ elementów tablicy. W ten sposób nie musimy wielokrotnie określać typu i nie ma ryzyka przypadkowego wpisania (i „błędnego wpisania”, ha!)

Oto ten sam przykład co powyżej, ale użycie auto jako typ num:

#include <iostream>
#include <vector>

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (auto num : fibonacci) // compiler will deduce type of num to be `int`
       std::cout << num << ' ';

    std::cout << '\n';

    return 0;
}

Ponieważ std::vector fibonacci ma elementy typu int, num zostanie wywnioskowane, że jest int.

Najlepsza praktyka

Użyj odliczenia typu (auto) z pętle for oparte na zakresie, aby kompilator wydedukował typ elementu tablicy.

Kolejną korzyścią wynikającą z użycia auto jest to, że jeśli typ elementu tablicy zostanie kiedykolwiek zaktualizowany (np. z int Do long), auto automatycznie wydedukuje typ zaktualizowanego elementu, zapewniając jego synchronizację (i zapobiegając konwersji typu).

Unikaj kopiowania elementów za pomocą odniesień

Rozważ następującą pętlę for opartą na zakresie, która iteruje po tablicy std::string:

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };

    for (auto word : words)
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

Dla każdej iteracji tej pętli zostanie przypisany następny std::string element z tablicy words (skopiowane) do zmiennej word. Kopiowanie std::string jest kosztowne, dlatego zazwyczaj przekazujemy std::string do funkcji poprzez odwołanie do stałej. Chcemy unikać tworzenia kopii rzeczy, których kopiowanie jest drogie, chyba że naprawdę potrzebujemy kopii. W tym przypadku po prostu drukujemy wartość kopii, a następnie kopia ulega zniszczeniu. Byłoby lepiej, gdybyśmy mogli uniknąć tworzenia kopii i po prostu odwoływać się do rzeczywistego elementu tablicy.

Na szczęście możemy to zrobić dokładnie, tworząc nasze element_declaration (stałe) odwołanie:

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };

    for (const auto& word : words) // word is now a const reference
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

W powyższym przykładzie word jest teraz stałym odniesieniem. Z każdą iteracją tej pętli word zostanie powiązany z następnym elementem tablicy. Dzięki temu możemy uzyskać dostęp do wartości elementu tablicy bez konieczności wykonywania kosztownej kopii.

Jeśli odniesienie nie jest stałe, można go również użyć do zmiany wartości w tablicy (co nie jest możliwe, jeśli nasz element_declaration jest kopią wartości).

Kiedy to zrobić użyj auto vs auto& vs const auto&

Zwykle używalibyśmy auto w przypadku taniego kopiowania typów, auto& kiedy chcemy modyfikować elementy i const auto& w przypadku typów, których kopiowanie jest kosztowne. Jednak w przypadku pętli for opartych na zakresach wielu programistów uważa, że ​​lepiej jest zawsze używać const auto& , ponieważ jest to bardziej przyszłościowe.

Rozważmy na przykład następujący przykład:

#include <iostream>
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "peter", "likes", "frozen", "yogurt" }; // elements are type std::string_view

    for (auto word : words) // We normally pass string_view by value, so we'll use auto here
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

W tym przykładzie mamy std::vector zawierający std::string_view obiekty. Ponieważ std::string_view jest zwykle przekazywana przez wartość, użycie auto wydaje się właściwe.

Zastanów się jednak, co się stanie, jeśli words zostanie później zaktualizowany do tablicy std::string .

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" }; // obvious we should update this

    for (auto word : words) // Probably not obvious we should update this too
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

Pętla for oparta na zakresie zostanie skompilowana i wykonana poprawnie, ale word zostanie teraz wywnioskowana, że jest to a std::string, a ponieważ jesteśmy używając auto, nasza pętla będzie po cichu tworzyć drogie kopie elementów std::string . Właśnie odnotowaliśmy ogromny spadek wydajności!

Istnieje kilka rozsądnych sposobów, aby zapewnić, że tak się nie stanie:

  • Nie używaj dedukcji typu w pętli for opartej na zakresie. Jeśli jawnie określiliśmy typ elementu jako std::string_view, to przy późniejszej aktualizacji tablicy do std::string, instrukcja std::string elementy zostaną domyślnie przekonwertowane na std::string_view, co nie stanowi problemu. Jeśli tablica zostanie zaktualizowana do innego typu, którego nie można konwertować, kompilator wyświetli błąd i będziemy mogli dowiedzieć się, co w tym momencie należy zrobić. Ale jeśli typ elementu jest konwertowalny, kompilator po cichu przeprowadzi konwersję i możemy nie zdawać sobie sprawy, że robimy coś nieoptymalnego.
  • Użyj const auto& zamiast auto jeśli używasz dedukcji typu w pętli for opartej na zakresie, gdy nie chcesz pracować z kopiami. Spadek wydajności wynikający z dostępu do elementów za pośrednictwem referencji, a nie wartości, będzie prawdopodobnie niewielki, co chroni nas przed potencjalnie znaczącymi spadkami wydajności w przyszłości, jeśli typ elementu zostanie później zmieniony na coś, co jest kosztowne w kopiowaniu.

Najlepsza praktyka

W przypadku pętli for opartych na zakresach preferuj zdefiniowanie typu elementu jako:

  • auto kiedy chcesz modyfikować kopie elementów.
  • auto& kiedy chcesz modyfikować oryginał elementy.
  • const auto& w przeciwnym razie (kiedy wystarczy wyświetlić oryginalne elementy).

Oparte na zakresie pętle i inne standardowe typy kontenerów

Oparte na zakresie pętle działają z szeroką gamą typów tablic, w tym (niezniszczonymi) tablicami w stylu C, std::array, std::vector, listami połączonymi, drzewami i mapami. Nie omówiliśmy jeszcze żadnego z nich, więc nie martw się, jeśli nie wiesz, co to jest. Pamiętaj tylko, że pętle for oparte na zakresie zapewniają elastyczny i ogólny sposób iteracji po czymś więcej niż tylko std::vector:

#include <array>
#include <iostream>

int main()
{
    std::array fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; // note use of std::array here

    for (auto number : fibonacci)
    {
        std::cout << number << ' ';
    }

    std::cout << '\n';

    return 0;
}

Dla zaawansowanych czytelników

Pętle for oparte na zakresie nie będą działać z uszkodzonymi tablicami w stylu C. Dzieje się tak dlatego, że pętla for oparta na zakresach musi znać długość tablicy, aby wiedzieć, kiedy przejście zostanie zakończone, a tablice w stylu C nie zawierają tej informacji.

Pętle for oparte na zakresach również nie będą działać z wyliczeniami. Metodę obejścia tego problemu pokazujemy na lekcji 17.6 — std::array i wyliczenia.

Uzyskiwanie indeksu bieżącego elementu

Pętle for oparte na zakresie nie zapewniają bezpośredni sposób uzyskania indeksu tablicy bieżącego elementu. Dzieje się tak dlatego, że wiele struktur, po których może iterować pętla for oparta na zakresach (np. std::list) nie obsługuje indeksów.

Jednakże, ponieważ pętle for oparte na zakresach zawsze iterują w przód i nie pomijają elementów, zawsze możesz zadeklarować (i zwiększyć) własny licznik. Jeśli jednak masz to zrobić, powinieneś rozważyć, czy nie lepiej będzie użyć zwykłej pętli for zamiast pętli for opartej na zakresie.

Pętle for oparte na zakresie w odwrotnej kolejności C++20

Pętle for oparte na zakresach iterują tylko w kolejności do przodu. Są jednak przypadki, w których chcemy przeglądać tablicę w odwrotnej kolejności. Przed wersją C++ 20 pętle for oparte na zakresach nie mogły być łatwo użyte do tego celu i należało zastosować inne rozwiązania (zwykle zwykłe pętle for).

Jednak od C++20 można używać std::views::reverse możliwości biblioteki Ranges do tworzenia odwróconego widoku elementów, przez które można przechodzić:

#include <iostream>
#include <ranges> // C++20
#include <string_view>
#include <vector>

int main()
{
    std::vector<std::string_view> words{ "Alex", "Bobby", "Chad", "Dave" }; // sorted in alphabetical order

    for (const auto& word : std::views::reverse(words)) // create a reverse view
        std::cout << word << ' ';

    std::cout << '\n';

    return 0;
}

Wypisuje:

Dave
Chad
Bobby
Alex

Nie omawialiśmy biblioteki zakresów, więc potraktuj to jako przydatny fragment magii.

Czas quizu

Pytanie nr 1

Zdefiniuj std::vector o następujących imionach: „Alex”, „Betty”, „Caroline”, „Dave”, „Emily”, „Fred”, „Greg” i „Holly”. Poproś użytkownika o podanie nazwy. Użyj pętli for opartej na zakresie, aby sprawdzić, czy nazwa wprowadzona przez użytkownika znajduje się w tablicy.

Przykładowe wyjście:

Enter a name: Betty
Betty was found.
Enter a name: Megatron
Megatron was not found.

Wskazówka: użyj std::string do przechowywania ciągu znaków wprowadzonego przez użytkownika.
Wskazówka: std::string_view można skopiować tanio.

Pokaż rozwiązanie

Pytanie nr 2

Zmodyfikuj rozwiązanie quizu 1. W tej wersji utwórz szablon funkcji (nie jest to zwykła funkcja) o nazwie isValueInArray() , który przyjmuje dwa parametry: a std::vector i wartość. Funkcja powinna zwrócić true jeżeli wartość znajduje się w tablicy, oraz false w przeciwnym wypadku. Wywołaj funkcję z main() i przekaż jej tablicę nazw oraz nazwę wprowadzoną przez użytkownika.

Przypomnienie:

  • Szablon funkcji korzystający z dedukcji argumentów szablonu (gdy argumenty typu szablonu nie są jawnie określone) nie wykona konwersji w celu dopasowania parametrów typu szablonu. Wywołanie albo pasuje do szablonu (i można wydedukować typ szablonu), albo nie.
  • Szablon funkcji z jawnie określonym argumentem typu szablonu przekonwertuje argumenty tak, aby pasowały do ​​typu parametru (ponieważ typ jest znany).

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