Iteratori

Iteratori su objekti pomoću kojih iteriramo kroz spremnike. Prisutni su u svim modernim jezicima, a specifičnost C++-a je što imaju sintaksu pokazivača. To znači da za kretanje kroz spremnik koristimo operatore ++ i --, a za dohvat elementa koristimo operator dereferenciranja; iteratore uspoređujemo pomoću operatora == i !=.

Svi spremnici imaju metode begin() i end() koje vraćaju iteratore za kretanje kroz spremnik:

  • begin() je iterator koji pokazuje na prvi element u spremniku,

  • end() je iterator koji pokazuje na jedno mjesto iza zadnjeg elementa u spremniku.

  • cbegin() je konstantan iterator koji pokazuje na prvi element u spremniku,

  • cend() je konstantan iterator koji pokazuje na jedno mjesto iza zadnjeg elementa u spremniku.

Konstantan iterator ne može mijenjati objekte kroz koje iterira. Oni su za njega konstantni.

Iteratori se dijele u kategorije prema svojim sposobnostima.

iterators.png
  • Iteratori nikad ne provjeravaju jesu li izašli izvan granica spremnika kroz koji iteriraju.

Kategorije iteratora

Svi iteratori dozvoljavaju kretanje unaprijed pomoću operatora ++ i dereferenciranje pooću operatora *. Derferencirani iterator može služiti samo za čitanje, samo za pisanje ili za oboje, ovisno o kategoriji kojoj pripada. K tome svi iteratori imaju operatore == i != za uspoređivanje.

Ulazni iterator (input iterator)

Ulazni iterator može se kretati samo unaprijed (nema operatora --) i može jedino čitati vrijednost sa ulaza. Kako služi uglavnom čitanju s ulaznog toka, ne može pročitati istu vrijednost više puta. Postinkrement operator (it++) ne mora nužno ništa vratiti.

Izlazni iterator (output iterator)

Izlazni iterator može se kretati samo unaprijed (nema operatora --) i može jedino pisati vrijednost na izlaz. Kako služi uglavnom za pisanje na izlazni tok, ne može pisati više puta u istu lokaciju. Izlazni iterator ne može biti konstantan.

Jednosmjerni iterator (forward iterator)

Jednosmjerni iterator može se kretati samo unaprijed (nema operatora --) i može pisati u lokaciju (ako nije konstantan) i čitati iz nje. Nema ograničenja na broj pisanja/čitanja. Takav iterator daje klasa std::forward_list<> i (eventualno) neuređeni asocijativni spremnici.

Dvosmjerni iterator (bidirectional iterator)

Dozvoljava kretanje u oba smjera pa ima operatore ++ i -- (prefiks i postfiks verziju). Može čitati i pisati (ako nije konstantan). Takav iterator daje klasa std::list<> i asocijativni spremnici.

Iterator izravnog dohvata (random access iterator)

Iteratori u ovoj kategoriji imaju sva svojstva dvosmjernih iteratora i k tome podržavaju pokazivačku aritmetiku. To znači da ako je it iterator izravnog dohvata koji pokazuje na neku lokaciju, onda it + n pokazuje na n-ti element od polazne lokacije. Dozvoljena je i sintaksa it[n]. Operacije dodavanja i oduzimanja cijelog broja iteratoru su dozvoljene i imaju kao efekt pomicanje iteratora za toliko mjesta unaprijed/unazad.

Funkcije na iteratorima

U zaglavlju <iterator> su definirane pomoćne funkcije koje rade s iteratorima:

Operacija Značenje

advance(it,n)

Pomiče iterator it za n pozicija unaprijed/unazad

newit = next(it)

Daje novi iterator newit koji pokazuje na sljedeću poziciju u odnosu na it. Iterator it se ne pomiče.

newit = next(it,n)

Daje novi iterator newit koji pokazuje na n-tu poziciju nakon it. Iterator it se ne pomiče.

newit = prev(it)

Daje novi iterator newit koji pokazuje na prethodnu poziciju u odnosu na it. Iterator it se ne pomiče.

newit = prev(it,n)

Daje novi iterator newit koji pokazuje na n-tu poziciju prije it. Iterator it se ne pomiče.

n = distance(it1,it2)

Cjelobrojna vrijednost n je udaljenost između pozicija na koje pokazuju iteratori it1 i it2. Iterator it1 mora pokazivati na element ispred it2.

iter_swap(it1, it2)

Vrši zamjenu dva iteratora.

  • U pozivu n = distance(it1,it2) iterator it1 može pokazivati na element nakon elementa na koji pokazuje it2 u slučaju iteratora izravnog dohvata. U slučaju dvosmjernih iteratora to vodi na nedefinirano ponašanje.

  • Funkcija std::iter_swap je definirana u zaglavlju <algorithm>.

Primjer:

std::list<int> li{1,2,3,4,5,6,7,8,9};
std::iter_swap(li.begin(), std::prev(li.end())); // 9,2,3,4,5,6,7,8,1,

Reverzni iteratori

Reverzni iteratori služe za iteriranje kroz spremnik u obrnutom pretku, od kraja prema početku. Na primjer:

#include <deque>
#include <algorithm>

void print(int x){
  std::cout << x << ",";
}

int main()
{
  std::deque<int> deq{1,2,3,4,5,6,7,8,9};

  std::for_each(deq.begin(), deq.end(), print);
  std::cout << std::endl;  // 1,2,3,4,5,6,7,8,9,


  std::for_each(deq.rbegin(), deq.rend(), print);
  std::cout << std::endl; // 9,8,7,6,5,4,3,2,1,

  return 0;
}
  • Metode rbegin() i rend() vraćaju reverzne iteratore tipa spremnik::reverse_iterator ili, ako je reverzni iterator konstantan, spremnik::const_reverse_iterator. U našem primjeru spremnik=std::deque<int>.

  • Metode crbegin() i crend() vraćaju konstantne reverzne iteratore tipa spremnik::const_reverse_iterator.

Konverzija u reverzni iterator i obratno

  • Konstruktor reverznog iteratora može se inicijalizirati običnim iteratorom i tada konstruktor vrši konverziju običnog u reverzni iterator. Dobiveni reverzni iterator pokazuje na jedno mjesto ispred običnog iteratora.

  • Funkcija base() na reverznom iteratoru vrši konverziju u obični iterator. Dobiveni obični iterator pokazuje na jedno mjesto iza reverznog iteratora.

std::deque<int> deq{1,2,3,4,5,6,7,8,9};
auto it = deq.begin() + 5;
std::deque<int>::reverse_iterator rit{it};  // konverzija u reverzni iterator
std::cout << *it << "\n";  // ispisuje 6
std::cout << *rit << "\n"; // ispisuje 5
auto rrit = rit.base();                     // konverzija u običan iterator
std::cout << *rrit << "\n"; // ispisuje 6

Ovakva konstrukcija čuva raspone pri prijelazu iz običnog u reverzno iteriranje:

auto it1 = deq.begin() + 2;
auto it2 = deq.begin() + 7;
std::for_each(it1, it2, print);
std::cout << std::endl;  // 3,4,5,6,7,

std::deque<int>::reverse_iterator rit1{it1}, rit2{it2};
std::for_each(rit2, rit1, print);
std::cout << std::endl;  // 7,6,5,4,3,
  • Reverzan iterator se prirodno implementira tako da fizički pokazuje na istu memorijsku lokaciju kao i odgovarajući običan iterator, ali logički pokazuje na lokaciju ispred nje.

Insert iteratori

Insert iteratori su posebni iteratori koji pripadaju kategoriji izlaznih iteratora koji prilikom dereferenciranja insertiraju element u spremnik. Ta se operacija može obavljati pomoći metoda:push_back(), push_front() i insert(), pa imamo tri vrste takvih iteratora:

  • std::back_insert_iterator<Spremnik>

  • std::front_insert_iterator<Spremnik>

  • std::insert_iterator<Spremnik>

Svi se oni nalaze u zaglavlju <iterators> i uzimaju kao parametar predloška tip spremnika na kojem rade. Njihovi konstruktori uzimaju spremnik, a insert_iterator uzima i poziciju ubacivanja. Na primjer,

std::list<int> lst;
std::back_insert_iterator<std::list<int> > iter_b(lst);
std::front_insert_iterator<std::list<int> > iter_f(lst);
std::insert_iterator<std::list<int> > iter(lst, lst.begin());

Ovi iteratori su konstruirani tako da operator ++ ne radi ništa, operator dereferenciranja vraća *this (dakle ne radi ništa) i tek operator pridruživanja poziva odgovarajuću insert metodu.

//Ubacivanje na kraj spremnika
*iter_b = 1;
iter_b++;
*iter_b = 2;
iter_b++;
*iter_b = 3;  // daje 1 2 3
// Ubacivanje ispred prvog elementa
*iter_f = 4;  // iter_f++ nije potreban jer ne radi ništa
*iter_f = 5;
*iter_f = 6;  // daje 6 5 4 1 2 3

Pokažimo još i insert_iterator:

auto it = std::find(lst.begin(), lst.end(), 3);
std::insert_iterator<std::list<int> > iter(lst, it);
*iter = 10;
*iter = 11;
*iter = 12;  // daje 6 5 4 1 2 10 11 12 3

Uočimo da back_insert_iterator i insert_iterator ubacuju elemente u ispravnom poretku dok ih front_insert_iterator ubacuje u inverznom poretku.

Pomoćne funkcije - inserteri

Postoje tri pomoćne funkcije koje uzimaju spremnik i kreiraju odgovarajući insert iterator. To su

Definirane su u zaglavlju <iterator>. Ove funkcje najčešće koristimo zajedno sa algoritmima. Na primjer:

std::vector<int> vec1;
std::copy(lst.begin(), lst.end(), std::back_inserter(vec1));
// daje  6 5 4 1 2 10 11 12 3

std::deque<int> deq;
std::copy(lst.begin(), lst.end(), std::front_inserter(deq));
// daje 3 12 11 10 2 1 4 5 6

std::list<int> lst1{lst};
std::copy(lst.begin(), lst.end(),
          std::inserter(lst1, std::next(lst1.begin(),3)));
// daje 6 5 4 6 5 4 1 2 10 11 12 3 1 2 10 11 12 3

Iostream iteratori (1)

Postoje dva specijalna iteratora (deklarirana u zaglavlju <iterator>) koja služe za čitanje s ulaznog streama i za pisanje na izlazni stream. Oba iteratora promatraju stream kao niz vrijednosti istog tipa; to su istream_iterator i ostream_iterator .

ostream_iterator

Kod ostream_iterator-a operator dereferenciranja (*) i operator inkrementiranja (++) ne rade ništa. Vrijednost na stream ispisuje operator pridruživanja. Ovdje nemamo end-iteratora,ali konstruktor iteratora pored streama mora kao argument dobiti i separator podataka (kao C-string). Za pisanje se koristi operator <<.

Operacija Značenje

ostream_iterator<T>(ostream, delim)

Konstruktor

*it

NoOp

it = value

ostream << value

it++

NoOp

++it

NoOp

// ostream iteratori
std::ostream_iterator<float> osi(std::cout, "\n");
//    std::ostream_iterator<float> osi_end;  -- ne postoji !
// ispis vektora
std::copy(fvec2.begin(), fvec2.end(), osi);
// drugi način da se postigne ispis:
for(auto & x : fvec2)
       *osi++ = x;  // dereferenciranje i inkrementiranje ne rade ništa
// dovoljno je pisati
for(auto & x : fvec2)
       osi = x;    // ispis radi operator pridruživanja

return 0;
}
  • U konstrukciji delim služi kao delimiter između dva ispisa. Može se izostaviti ako se želi ispis bez razgraničenja između članova u ispisu. Tip mu je const char *.

Iostream iteratori (2)

istream_iterator

istream_iterator služi čitanju podataka sa ulaznog streama.

Kod istream_iterator-a operator dereferenciranja vraća pročitanu vrijednost, a operator inkrementiranja (++) čita sljedeću vrijednost. Čitanje završava kada se dođe do kraja streama ili kada dođe do greške. Za čitanje se koristi operator >>. Prilikom unosa sa tastature kraj unosa se daje s Return i Ctrl-D (linux) ili s Return i Ctrl-Z (windows).

Operacija Značenje

istream_iterator<T>()

Dodijeljeni konstruktor. Predstavlja end iterator.

istream_iterator<T>(ostream)

Konstruktor. Kreira istream i (eventualno) čita prvu vrijednost

*it

Vraća pročitanu vrijednost

it++

Čita sljedeću vrijednost.

++it

Čita sljedeću vrijednost.

// istream iteratori
std::istream_iterator<float> isi(std::cin);
std::istream_iterator<float> isi_end;

std::vector<float> fvec;
// dereferenciranje operatora vraća pročitanu vrijednost. Operator ++
// čita sljedeću vrijednost
while(isi != isi_end){
    fvec.push_back(*isi++);
}

// Malo korisniji primjer -- čitanje iz datoteke.
std::ifstream in("brojevi.txt");
std::istream_iterator<float> isi_in(in);

// čitanje pri konstrukciji vektor.
std::vector<float> fvec2(isi_in, isi_end);

Obevrijeđivanje iteratora

Neke operacije mogu obezvrijediti iteratore, pokazivače i reference na elemente. Jedna takva operacija je brisanje elementa. Kod vektora pri brisanju i umetanju elementa gube vrijednost svi iteratori, pokazivači i reference nakon upisanog/obrisanog elementa. Na primjer, sljedeći kod je neispravan:

auto badValue = [](char c){ return c > 'k';};

for(auto it = s1.begin(); it != s1.end(); ++it){
   if(badValue(*it)) s1.erase(it); // Neodređeno ponašanje -- it obezvrijeđen
}

Funkcija erase() kod vektora obezvrijeđuje iterator na obrisani element i kasnije iteratore. Ispravna verzija bi bila:

for(auto it = s1.begin(); it != s1.end(); ++it){
   if(badValue(*it))
       s1.erase(it--); // smanji it prije no što staru vrijednost predaš funkciji erase
}

Nije samo brisanje elementa operacija koja može obezvrijediti iterator. Kod ubacivanja novog elementa u vektor mijenja se end iterator, a ako se pri tome vektor mora realocirati zbog nedostatka prostora obezvrijeđuju se svi iteratori.

Kod deque spremnika svi iteratori mogu izgubiti vrijednost kod brisanja i ubacivanja elementa. Jedino brisanje ili ubacivanje elementa na jednom od krajeva spremnika ne obezvrijeđuje iteratore (osim onog obrisanog elementa).

Kod liste i asocijativnih spremnika (map, set, multimap, multiset) brisanje obezvrijeđuje samo iterator na obrisani element, dok ubacivanje ne obezvrijeđuje iteratore.