Pętle, których nie musisz pisać
Na poprzedniej stronie widać było, że każdy kontener udostępnia iteratory, czyli lekkie kursory z begin() i end(). Ta abstrakcja to cały powód istnienia standardowych algorytmów. Zamiast pisać surową pętlę for za każdym razem, gdy chcesz coś wyszukać, policzyć lub przekształcić, wywołujesz nazwaną funkcję z <algorithm> i przekazujesz jej zakres.
Zakres to po prostu dwa iteratory: gdzie zacząć i miejsce tuż za końcem. Ponieważ każdy algorytm mówi tym samym językiem iteratorów, to samo find działa na vector, string i zwykłej tablicy.
Wzorzec do zapamiętania: algorytm wyszukujący zwraca iterator end(), żeby powiedzieć "nic nie znaleziono". Zawsze porównuj wynik z end(), zanim go wyłuskasz, bo wyłuskanie end() to niezdefiniowane zachowanie.
Liczenie i testowanie predykatami
Wiele algorytmów przyjmuje predykat, czyli funkcję (zwykle lambdę), która dla każdego elementu zwraca bool. count_if liczy trafienia; all_of, any_of i none_of odpowiadają tak lub nie na pytania o cały zakres.
std::count (bez _if) to prostszy krewny, który liczy konkretną wartość zamiast warunku. Po wersje z predykatem sięgaj w chwili, gdy twój test brzmi "wszystko, co spełnia regułę", a nie "ta konkretna wartość".
Przekształcanie i redukcja zakresu
Dwa woły robocze pokrywają większość przetwarzania danych: std::transform przepuszcza każdy element przez funkcję, a std::accumulate (z <numeric>, nie z <algorithm>) zwija zakres do jednej wartości.
transform zapisuje wyniki przez iterator wyjściowy. Częsty i groźny błąd to skierowanie wyjścia do pustego wektora: algorytm zakłada, że miejsce już istnieje, i pisze za końcem. Albo najpierw ustaw rozmiar celu, albo użyj back_inserter, żeby każdy wynik trafiał tam przez push_back.
accumulate zaczyna od wartości początkowej i łączy elementy od lewej do prawej. Typ wartości początkowej ma znaczenie: przekaż 0 (czyli int), a suma będzie liczona w int, co może spowodować przepełnienie lub obcięcie, zależnie od typu danych elementów.
Gdyby zamiast tego stało accumulate(prices.begin(), prices.end(), 0) z wartością startową typu int, każde dodawanie odbywałoby się w int, a grosze by zniknęły. Typ wartości startowej po cichu wyznacza typ wyniku.
Idiom erase-remove
Oto pułapka, która zaskakuje wszystkich. std::remove nie usuwa niczego z kontenera. Algorytmy mają tylko iteratory, więc nie mogą zmienić rozmiaru kontenera; nawet nie wiedzą, że jakiś kontener istnieje. remove w rzeczywistości przesuwa wszystkie zachowane elementy na początek, zostawia ogon w nieokreślonym stanie i zwraca iterator do nowego logicznego końca.
// samo remove nie zmienia rozmiaru, oto błąd:
remove(v.begin(), v.end(), 0); // zwraca iterator, który zignorowano
// v ma nadal pierwotny rozmiar; ogon to śmieci
Aby naprawdę usunąć elementy, łączysz remove z metodą erase kontenera, i stąd nazwa idiom erase-remove:
Użyj remove_if, gdy zamiast konkretnej wartości masz predykat. W C++20 możesz całkiem pominąć ten taniec dzięki wolnym funkcjom std::erase / std::erase_if, które wykonują oba kroki za ciebie: erase(v, 0);.
Iteratory żyjące dłużej niż ich ważność
Ponieważ algorytmy zwracają iteratory, podlegają one tym samym regułom unieważniania, które znasz z poprzedniej strony. Zapisanie iteratora, a potem modyfikacja kontenera (push_back, który wywoła realokację, albo erase) może zostawić zapisany iterator wiszący, a użycie go to niezdefiniowane zachowanie.
auto it = find(v.begin(), v.end(), 16);
v.push_back(99); // może przenieść pamięć v
cout << *it; // BŁĄD: `it` może teraz wskazywać zwolnioną pamięć
Bezpieczny nawyk: używaj iteratora zwróconego przez algorytm od razu, przed jakąkolwiek operacją, która mogłaby zmienić rozmiar kontenera lub przenieść jego pamięć. Jeśli musisz zmienić kontener na podstawie wyniku, zapamiętaj indeks (it - v.begin()), bo indeksy przetrwają realokację.
Dalej: sortowanie
Znasz już wyszukiwanie, liczenie, przekształcanie i redukcję, ale jeden algorytm jest na tyle ważny, że zasługuje na osobną stronę. std::sort porządkuje zakres w miejscu, a gdy dane są posortowane, otwiera się cała rodzina szybszych algorytmów dla danych posortowanych (binary_search, lower_bound, equal_range). Następnie zajmiemy się sortowaniem: jak podać własny komparator, czym różni się sort od stable_sort i jakich reguł musi przestrzegać funkcja porównująca, aby uniknąć niezdefiniowanego zachowania.
Najczęściej zadawane pytania
Czym jest nagłówek <algorithm> w C++?
<algorithm> to nagłówek biblioteki standardowej z funkcjami generycznymi, takimi jak std::find, std::sort, std::count_if i std::transform. Działają one na zakresach opisanych parą iteratorów (zwykle begin() i end()), więc ten sam algorytm działa na vector, array, string i każdym kontenerze, który udostępnia iteratory.
Jak sprawdzić, czy wartość istnieje w wektorze w C++?
Użyj std::find: auto it = find(v.begin(), v.end(), target);. Jeśli it == v.end(), wartości nie ma; w przeciwnym razie it wskazuje pierwsze trafienie. Aby sprawdzić warunek zamiast konkretnej wartości, użyj std::any_of z predykatem.
Dlaczego std::remove tak naprawdę nie usuwa elementów z kontenera?
Algorytmy widzą tylko iteratory, a nie kontener, więc std::remove nie może go zmniejszyć. Przesuwa zachowane elementy na początek i zwraca iterator do nowego logicznego końca. Musisz potem wywołać v.erase(...) (idiom erase-remove), aby fizycznie pozbyć się resztek.