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_inserter nie rezerwuje pojemności. Wywołuje push_back, więc std::vector może realokować w trakcie algorytmu.
  • move_iterator zostawia 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.