Adapter iteratora opakowuje inny iterator i zmienia jego zachowanie. Biblioteka standardowa ma ich kilka i są dobrym miejscem, żeby zobaczyć Adapter bez hierarchii klas. Celem jest protokół iteratora. Adaptowanym obiektem jest kontener, strumień albo inny iterator. Adapterem jest mały typ wartościowy, który można kopiować i składać.
std::reverse_iterator
std::reverse_iterator<It> przechowuje iterator przechodzący do przodu i odwraca kierunek. operator++ przesuwa iterator bazowy do tyłu, a dereferencja czyta element przed zapisaną pozycją.
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> values{1, 2, 3, 4, 5};
for (auto it = std::make_reverse_iterator(values.end());
it != std::make_reverse_iterator(values.begin()); ++it) {
std::cout << *it << ' ';
}
std::cout << '\n';
}
Wynik:
5 4 3 2 1
Przesunięcie ma znaczenie, gdy mieszasz iterator odwrotny z jego iteratorem bazowym. base() wskazuje o jedno miejsce za element, na który wskazuje iterator odwrotny:
std::reverse_iterator<std::vector<int>::iterator> last(values.end());
std::cout << *last << '\n'; // 5
std::cout << *(last.base() - 1) << '\n'; // 5
std::back_insert_iterator
Algorytmy piszą przez iteratory wyjściowe za pomocą przypisania. Kontener nie jest iteratorem wyjściowym, ale std::back_insert_iterator go adaptuje. Przypisanie wywołuje push_back, więc kontener rośnie w miarę zapisu.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> input{1, 2, 3, 4, 5, 6};
std::vector<int> evens;
std::copy_if(input.begin(), input.end(),
std::back_inserter(evens),
[](int value) { return value % 2 == 0; });
for (int value : evens) {
std::cout << value << ' ';
}
std::cout << '\n';
}
Wynik:
2 4 6
std::front_inserter wywołuje push_front, więc potrzebuje kontenera z tą operacją, na przykład std::list albo std::deque. std::inserter wywołuje insert na podanej pozycji.
std::move_iterator
Dereferencja zwykłego iteratora daje referencję. Dereferencja std::move_iterator daje referencję do rvalue, więc algorytmy i konstruktory przenoszą zamiast kopiować.
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
int main() {
std::vector<std::string> source{"one", "two", "three"};
std::vector<std::string> target(
std::make_move_iterator(source.begin()),
std::make_move_iterator(source.end()));
for (const auto& value : target) {
std::cout << value << ' ';
}
std::cout << '\n';
}
Łańcuchy trafiają do target, a elementy w source zostają w poprawnym, ale nieokreślonym stanie. O to chodzi w tym adapterze i dlatego trzeba go używać ostrożnie.
Iteratory strumieniowe
std::istream_iterator<T> przedstawia sformatowane wejście jako iterator wejściowy. std::ostream_iterator<T> przedstawia iterator wyjściowy, który pisze do strumienia. Dzięki nim algorytmy czytają i piszą strumienie bez ręcznej pętli.
#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
int main() {
std::vector<int> values;
std::copy(std::istream_iterator<int>(std::cin),
std::istream_iterator<int>(),
std::back_inserter(values));
std::copy(values.begin(), values.end(),
std::ostream_iterator<int>(std::cout, "\n"));
}
Iteratory strumieniowe są jednoprzebiegowe. Parsują tekst sformatowany, więc błędny token ustawia strumień w stan błędu. Sprawdź strumień po kopiowaniu, jeśli dane pochodzą od użytkownika albo z pliku.
Tabela adapterów
| Adapter standardowy | Opakowuje | Co zmienia |
|---|---|---|
std::reverse_iterator<It> |
iterator dwukierunkowy | przechodzenie biegnie do tyłu |
std::back_insert_iterator<C> |
kontener z push_back |
przypisanie dopisuje na końcu |
std::front_insert_iterator<C> |
kontener z push_front |
przypisanie dopisuje na początku |
std::insert_iterator<C> |
kontener z insert |
przypisanie wstawia na pozycji |
std::move_iterator<It> |
dowolny iterator | dereferencja daje referencję do rvalue |
std::istream_iterator<T> |
strumień wejściowy | strumień staje się iteratorem wejściowym |
std::ostream_iterator<T> |
strumień wyjściowy | przypisanie pisze do strumienia |
std::counted_iterator<It> |
iterator i licznik | iterator niesie ograniczenie |
std::common_iterator<I, S> |
iterator i sentinel | oba stają się jednym typem iteratora |
Dwa ostatnie są z C++20. Adaptują pary iteratorów i sentineli dla biblioteki ranges.
Dlaczego to Adapter
Klientem jest algorytm, na przykład std::copy albo std::copy_if. Oczekuje protokołu iteratora: inkrementacji, dereferencji, porównania i przypisania. Adaptowanym obiektem jest kontener, strumień albo inny iterator, który tego protokołu nie daje bezpośrednio. Adapterem jest opakowanie iteratora.
W typowym przypadku nie ma dyspozycji wirtualnej ani alokacji na stercie. Adapter jest szablonowym typem wartościowym, więc kompilator może wstawić przekazywanie. Dzięki temu adaptery iteratorów są dość tanie, żeby używać ich w ciasnych pętlach.
Na co uważać
reverse_iterator::base()jest przesunięty o jeden. Nie zakładaj, że wskazuje element, który właśnie odczytałeś.std::back_inserternie rezerwuje pojemności. Wywołujepush_back, więcstd::vectormoże realokować w trakcie algorytmu.move_iteratorzostawia przeniesione elementy. Nie czytaj ich tak, jakby nadal miały stare wartości.- Iteratory strumieniowe są jednoprzebiegowe i związane ze sformatowanym I/O. Nadają się do przykładów i prostych narzędzi, nie do danych binarnych.
- Adaptory zakresów, takie jak
std::views::reverse, to osobna warstwa. Składają się leniwie i zachowują bardziej jak Decorator niż Adapter.
Podsumowanie
Adaptery iteratorów to najczystsze przykłady Adaptera w bibliotece standardowej. Każdy opakowuje istniejący typ i sprawia, że spełnia protokół iteratora, którego używają algorytmy. Nie kopiują kontenera ani strumienia i nie dodają hierarchii klas. Kiedy piszesz własne opakowanie iteratora, trzymaj się tej samej zasady: zmieniaj interfejs, nie dane.