Download as pdf or txt
Download as pdf or txt
You are on page 1of 12

Dr.

eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

Predavanje 7_b
Dijeljeni pametni pokazivai o kojima smo govorili zaista su veoma korisni, i u 90 % praktinih
situacija u potpunosti nas rjeavaju problema sa curenjem memorije (onih preostalih 10 % upoznaemo
neto kasnije) i openito problema upravljanja memorijom. to je posebno vano, oni nita nisu manje
efikasni sa aspekta brzine rada sa njima od obinih pokazivaa. Ipak, oni zauzimaju znatno vie prostora
od obinih pokazivaa (pored same informacije o adresi objekta, oni uvaju i adresu upravljakog bloka
koji sadri broja pristupa objektu i jo neke informacije). S druge strane, u mnogim primjenama znamo
da nam ne treba da vie pokazivaa pokazuje na isti objekat. U takvim sluajevima, umjesto dijeljenjih
pametnih pokazivaa treba koristiti tzv. jedinstvene pametne pokazivae (engl. unique smart pointers)
koji nude slinu funkcionalnost, ali nemaju nikakvih dodatnih memorijskih optereenja u odnosu na
klasine pokazivae. Bitna stvar za ove pametne pokazivae je da je nemogue da vie takvih pokazivaa
pokazuje na isti objekat (sintaksa nam prosto ne dozvoljava da to uradimo).
Jedinstveni pametni pokazivai deklariraju se slino kao i dijeljeni pametni pokazivai, samo se
umjesto shared_ptr navodi unique_ptr. Naalost, ne postoji neka funkcija analogna funkciji
make_shared koja bi umjesto dijeljenog davala jedinstveni pametni pokaziva, nego se mora koristiti
konverzija iz glupog pokazivaa (inae, takva funkcija pod nazivom make_unique planirana je za
C++14). Na primjer:
std::unique_ptr<double> p(new double(3.5));
p = std::unique_ptr<double>(new double(2.13));

// Preusmjeravanje...

Meutim, sutinska razlika je to se jedinstveni pametni pokaziva ne moe inicijalizirati drugim


jedinstvenim pokazivaem, niti mu se moe dodijeliti drugi jedinstveni pokaziva:
std::unique_ptr<double> p1(new double(3.5));
std::unique_ptr<double> p2(p1);
std::unique_ptr<double> p3;
p3 = p1;

// SINTAKSNA GREKA!
// TAKOER...

Na ovaj nain je obezbijeeno da se ne moe desiti da dva jedinstvena pametna pokazivaa


pokazuju na isti objekat. S druge strane mogue je pomjeriti (koristei move funkciju) jedan jedinstveni
pokaziva u drugi, pri tome onaj prvi gubi kontrolu nad objektom koji je pokazivao u korist drugog,
dok on sam postaje nul-pokaziva. Na primjer:
std::unique_ptr<double> p1(new double(3.5));
std::unique_ptr<double> p2(std::move(p1));
std::unique_ptr<double> p3;
p3 = std::move(p2);

// p2 uzima kontrolu
//
(a p1 gubi)...
// p3 uzima kontrolu

Jedinstveni pametni pokazivai ne mogu se kopirati u dijeljene pametne pokazivae, ali se mogu
pomjeriti u njih (nakon ega oni takoer gube kontrolu nad pripadnim objektom i postaju nul-pokazivai).
Slijedi primjer koji pokazuje ta jeste a ta nije legalno:
std::unique_ptr<double>
std::shared_ptr<double>
std::shared_ptr<double>
std::shared_ptr<double>
p3 = p1;
p3 = std::move(p1);

p1(new double(3.5));
p2(p1);
p2(std::move(p1));
p3;

// ILEGALNO!
// OK...
// ILEGALNO!
// OK...

Interesantno je da se bilo jedinstveni bilo dijeljeni pametni pokaziva moe inicijalizirati rezultatom
funkcije koja vraa jedinstveni pametni pokaziva. Isto ako, moe im se dodijeliti rezultat funkcije koja
vraa jedinstveni pametni pokaziva. Naime, tehniki gledano, prenoenje rezultata pozvane funkcije na
mjesto poziva svakako se smatra kao pomjeranje a ne kopiranje, s obzirom da objekat koji vraamo iz
funkcije svakako prestaje postojati im se funkcija zavri (ovo ne mora vrijediti ako se iz funkcije
vraaju reference, ali to ovdje nije sluaj). Stoga su primjer poput ovog sasvim legalan:

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

std::unique_ptr<double> Funkcija(double x) {
std::unique_ptr<double> p(new double(x));
return p;
}
...
std::shared_ptr<double> p2(Funkcija(3.5));
p2 = Funkcija(2.13);

// Legalno...
// I ovo je legalno...

Ova osobina je izuzetno korisna. Naime, ukoliko znamo da nam u nekoj funkciji ne treba da vie
pokazivaa pokazuje na isti dinaiki kreirani objekat, u njoj moemo koristiti jedinstveni pametni
pokaziva. Meutim, ukoliko takav pokaziva vratimo kao rezultat iz funkcije, moemo ga slobodno
ukoliko nam to treba dodijeliti dijeljenom pametnom pokazivau, to omoguava da izvan te funkcije
smije vie pametnih pokazivaa pokazivati na isti objekat. Na taj nain funkcija ne treba da se brine kako
e se pristupati objektu na koji pametni pokaziva pokazuje izvan nje same.
Jo jedna sitna razlika izmeu jedinstvenih i dijeljenih pametnih pokazivaa je to ovi prvi smiju
pokazivati i na dinamiki alocirane nizove, samo to to treba posebno naglasiti prilikom deklaracije, na
nain koji je vidljiv iz sljedeeg primjera (razlozi zbog kojih tako neto nije podrano i za dijeljene
pametne pokazivae prilino su kontraverzni):
std::unique_ptr<int[]> p(new int[5]);

// Obratiti panju na []...

Za pametne jedinstvene pokazivae koji pokazuju na dinamiki alocirane nizove podrano je i direktno
indeksiranje, tako da se preko njih moe pristupati nizu na koji pokazuju istom sintaksom kao da su oni
ba taj niz, isto kao to se moe raditi i sa obinim pokazivaima. Na primjer, za gore definirani pamati
pokaziva p legalno je pisati
for(int i = 0; i < 5; i++) p[i] = 0;

Ipak, pokazivaka aritmetika nije podrana ni za takve pametne pokazivae, sa ciljem da se sprijei da se
pametni pokazivai koriste kao iteratori. Kao to je ve reeno, iteratorsku i monikersku ulogu
pokazivaa treba striktno razdvojiti, a uloga pametnih pokazivaa je iskljuivo monikerska.
Kao to je ve reeno, u jeziku C++ upotrebu klasinih pokazivaa zbog njihovih ogranienja treba
smanjiti na nuni minimum. Sa aspekta iteratorske primjene, oni su glupi u smislu to se ne znaju
kretati kroz strukture podataka iji elementi nisu kontinualno rasporeeni u memoriji, dok sa aspekta
monikerske primjene, oni nemaju odgovornost prema objektu na koji pokazuju. Kao alternativu, za
monikerske primjene treba koristiti reference i pametne pokazivae, dok za iteratorske primjene treba
koristiti iteratore. Poto smo o referencama i pametnim pokazivaima do sada rekli dovoljno, u nastavku
emo detaljnije obraditi koncept iteratora, koji smo ranije samo dotakli.
U jeziku C++ se ne zahtijeva da neto podrava sve operacije koje podravaju klasini pokazivai
da bi se zvalo iteratorom. Tanije, minimum operacija koji treba biti podran za neki tip da bi se on zvao
iteratorom je da za njega bude podrano dereferenciranje (operator *), inkrementiranje (operator
++), dodjela (operator =), te poreenje na jednakost i razliitost (operatori == i !=). Pri tome,
ak se ne postavljaju ni precizni zahtjevi kako se rezultat njegovog dereferenciranja moe koristiti (npr.
ne insistira se da se rezultat dereferenciranja mora moi koristiti kao l-vrijednost). U zavisnosti od
operacija koje neki iteratorski tip podrava, iteratori se u jeziku C++ mogu razvrstati u 5 kategorija:
ulazni iteratori (engl. input iterators), izlazni iteratori (engl. output iterators), iteratori sa kretanjem
unaprijed (engl. forward iterators), dvosmjerni iteratori (engl. bidirectional iterators) i iteratori sa
direktnim (sluajnim) pristupom (engl. random access iterators).
Za ulazne iteratore ne zahtijeva se nita drugo osim gore navedenog minimuma operacija. Pored
toga, ne zahtijeva se da se rezultat dereferenciranja iteratora moe koristiti kao l-vrijednost (npr. ukoliko
je it ulazni iterator, a x neka promjenljiva onakvog tipa kakav je *it, za ulazni iterator se ne
zahtijeva da konstrukcija *it = x mora imati smisla, ali konstrukcija x = *it mora biti smislena).
S druge strane, za izlazne iteratore, pored gore navedenog minimuma operacija zahtijeva se da se
rezultat njihovog dereferenciranja mora moi koristiti kao l-vrijednost, mada se ne zahtijeva da rezultat
2

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

njihovog dereferenciranja mora biti smislen u sluajevima kada se on ne koristi kao l-vrijednost (npr.
konstrukcija *it = x mora biti smislena, ali konstrukcija x = *it ne mora nuno biti smislena).
Ukoliko je neki iterator istovremeno i ulazni i izlazni (tj. ukoliko se rezultat njegovog dereferenciranja
moe koristiti u ma kakvom kontekstu), takav iterator nazivamo iterator sa kretanjem unaprijed. Ukoliko
je za takav iterator pored operacije inkrementiranja podrana i operacija dekrementiranja (operator ),
govorimo o dvosmjernom iteratoru. Za dvosmjerne iteratore ne mora nuno biti podrana ostala
pokazivaka aritmetika niti indeksiranje (odnosno izrazi poput it + n, it += n, it1 it2,
it[n] itd. gdje je n neka cjelobrojna vrijednost ne moraju nuno imati smisla). Konano, ukoliko
neki iterator podrava sve aritmetike operacije koje podravaju i klasini pokazivai, ukljuujui i
mogunost poreenja po veliini sa operacijama poput (<, <=, > i >=), radi se o iteratoru sa
direktnim pristupom.
Iteratori sa kojima smo se do sada susretali (iteratori za vektor, string i dek) su svi bili sa direktnim
pristupom. Upoznaemo se sada sa nekim drugim kontejnerskim tipovima podataka iji iteratori nemaju
snagu iteratora sa direktnim pristupom. Na prvom mjestu, tu je tip podataka nazvan lista. Liste se
deklariraju slino kao vektori i dekovi, samo se koristi rije list umjesto vector ili deque, i
trae ukljuivanje istoimene biblioteke. Podravaju sve uglavnom sve iste operacije kao vektori i dekovi
(ukljuujui i push front), ali uz jedan bitan izuzetak: liste se ne mogu indeksirati. Na primjer:
std::list<int> lista{5, 2, 4, 6};
lista.push_back(3);
lista.push_front(8);
for(int x : lista) std::cout << x << " ";
std::cout << lista[2];
lista[1] = 10;

// Ispis 8 5 2 4 6 3
// NE MOE!
// NE MOE NI OVO!

S obzirom da se liste ne mogu indeksirati (uskoro emo vidjeti zato), jedini nain da pristupimo
njihovim elementima je pomou iteratora. Recimo, sljedei primjer e ispisati prva 3 elementa liste, a
zatim e prei u novi red i ispisati ostale elemente liste:
std::list<int>::iterator it(lista.begin());
for(int i = 0; i < 3; i++)
std::cout << *it++ << " ";
std::cout << std::endl;
while(it != lista.end())
std::cout << *it++ << " ";

// ili auto it(...):


// Prva 3 elementa
// Novi red
// Ostali elementi

Meutim, iteratori za listu nisu iteratori sa direktnim pristupom, nego samo dvosmjerni iteratori. Stoga
za njih nisu definirane nikakve operacije pokazivake aritmetike osim ++ i , niti ikakva poreenja
osim na jednakost i razliitost (stoga je bilo bitno da u while petlji koristimo operator != a ne <).
Tako sljedei pokuaj da direktno ispiemo recimo trei element liste nee raditi (jer konstrukcija
lista.begin() + 2 nije legalna):
std::cout << *(lista.begin() + 2);

// OVO NE MOE!

Drugim rijeima, kod listi moemo pristupiti samo onom elementu na koji neki iterator trenutno
pokazuje, a iterator moemo pomjerati u jednom koraku iskljuivo za jedno mjesto naprijed ili nazad.
Postavlja se pitanje emu slue ovako ogranieni tipovi podataka. Poenta je u sljedeem. Elementi liste
su namjerno razbacani po memoriji (tj. nisu smjeteni jedan do drugog), iz razloga koji e uskoro biti
jasni. Meutim, uz svaki element liste interno se uvaju dva pokazivaa koji pokazuju gdje se u
memoriji nalazi prethodni odnosno sljedei element liste ukoliko takvih ima, tj. ukoliko nismo na kraju
liste (u teoriji podataka kae se da se radi o dvostruko povezanoj listi, engl. double linked list). Iteratori
za listu te informacije koriste da pronau gdje se nalazi sljedei ili prethodni element u odnosu na
element na koji trenutno pokazuju. Meutim, te informacije su naravno nedovoljne da se brzo locira gdje
se nalazi element koji je recimo 10 mjesta unaprijed ili unazad u odnosu na tekuu poziciju (jedini nain
da se pomjerimo 10 pozicija unaprijed je da runo ili u petlji izvrimo 10 pomjeranja za po jedno mjesto
unaprijed, jer drugaije ne moemo saznati gdje se nalazi taj element). Stoga ostale operacije
pokazivake aritmetike (osim ++ i ) nisu podrane, jer se ne bi mogle podrati efikasno, a
programer vjerovatno ne bi bio svjesan da se one ne izvode efikasno. Iz istog razloga (nemogunost
3

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

efikasne podrke) nije podrano ni indeksiranje. S druge strane, operacija umetanja novog elementa
(pomou funkcije insert) na poziciju gdje trenutno iterator pokazuje vri se izuzetno efikasno, jer
nije potrebno nikakvo pomjeranje elemenata koji slijede u memoriji. Zaista, novi element koji se umee
moe se staviti bilo gdje u memoriju, a samo ga je potrebno uvezati u lanac korigiranjem internih
pokazivaa u elementu koji mu prethodi i elementu koji slijedi nakon njega. Slino, brisanje elementa
liste na koje iterator trenutno pokazuje (pomou funkcije erase) izvodi se izuzetno efikasno, jer se
opet sve svodi samo na korekciju internih pokazivaa. Ovo je bitna razlika u odnosu na nizove i dekove,
kod kojih umetanje i brisanje elemenata na proizvoljnoj poziciji radi vrlo neefikasno, zbog potrebe za
njihovim premjetanjem u memoriji. Stoga liste treba koristiti u sluajevima kada nam je od velike
vanosti mogunost efikasnog umetanja odnosno brisanja elemenata na proizvoljnoj poziciji, a
mogunost indeksiranja nam nije od velikog znaaja (ima mnogo primjena u kojima se elementi
obrauju sekvencijalno, po redu, pa nam tada indeksiranje nije ni potrebno). Slijedi primjer u kojem se
sa tastature unosi niz brojeva sa tastature sve dok se ne unese nula. Za svaki uneseni broj trai se mjesto
gdje bi njega trebalo ubaciti u listu da lista bude u rastuem poretku brojeva, i kada se nae to mjesto,
uneseni broj se umee na to mjesto. Na taj nain se lista uvijek odrava u sortiranom poretku. Nakon to
se unos okona, ispisuje se itava lista, za koju se moemo uvjeriti da je u rastuem poretku:
std::list<int> lista;
for(;;) {
int x;
std::cin >> x;
if(x == 0) break;
std::list<int>::iterator it(lista.begin());
while(it != lista.end() && *it < x) it++;
lista.insert(it, x);
}
for(auto x : lista) std::cout << x << " ";

// moe krae sa "auto"


// Nai pravo mjesto
// Umetni element

Prethodna while petlja zajedno sa deklaracijom iteratora ispred nje moe se kompaktnije zapisati u
vidu for petlje sa praznim tijelom:
for(auto it = lista.begin(); it != lista.end() && *it < x; it++);

Vidimo da liste imaju itekako svoju upotrebnu vrijednost. Meutim, pamenje dva interna
pokazivaa po svakom elementu je cijena koja je plaena za omoguavanje brzog umetanja i brisanja. U
nekim primjenama znamo da emo se kroz listu kretati samo unaprijed, tako da su interni pokazivai
koji pokazuju na prethodni element isti viak. Stoga je u C++11 uvedena podverzija tipa lista nazvana
jednosmjerna lista (engl. forward list) odnosno jednostruko povezana lista (engl. single linked list). Kod
ove liste, svaki element uva informaciju samo o tome gdje se nalazi sljedei, ali ne i prethodni element.
Tip ove liste je forward_list i definirana je u istoimenoj biblioteci. Iterator koji se kree kroz ovu
listu nije dvosmjerni iterator, nego iterator sa kretanjem unaprijed, i on ne podrava operator .
Ukoliko znamo da se kroz listu neemo kretati unaprijed, koritenjem jednosmjerne liste moemo
utediti jedan interni pokaziva po svakom elementu liste. Recimo, prethodni programski isjeak e bez
problema raditi ukoliko list zamijenimo sa forward_list.
Jo dva zanimljiva kontejnerska tipa podataka iji su iteratori dvosmjerni ali nisu sa direktnim
pristupom su skup (engl. set) i multiskup (engl. multiset). Odgovarajua imena ovih tipova u C++ su
set i multiset, a oba su definirana u biblioteci imena set. Skupovi i multiskupovi slini su listi
u smislu da se i oni ne mogu indeksirati, ali za razliku od liste, elementi u skupu nemaju poziciju (dakle,
pojmovi poput prvi, trei, posljednji itd. element nemaju smisla). Element jeste ili nije u skupu (odnosno
multiskupu), i to je sve. Stoga, operacije poput push_back i push_front za skupove odnosno
multiskupove nemaju smisla, jer pojmovi kraj i poetak nemaju smisla. To omoguava skupovima i
multiskupovima da svoje elemente interno uvaju u sortiranom poretku. Drugim rijeima, neovisno od
redoslijeda kojim se elementi ubacuju u skup ili multiskup, oni se uvijek obrauju u sortiranom poretku.
Ovo omoguava vrlo brzo pronalaenje da li se neki element nalazi ili ne nalazi u skupu ili multiskupu,
zasnovanu na binarnoj pretrazi. Inae, jedina razlika izmeu skupova i multiskupova je u tome to
multiskupovi dozvoljavaju viestruko ponavljanje istog elementa, dok se kod skupova svaki pokuaj
umetanja elementa koji ve postoji u skupu ignorira. Na primjer:

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

std::set<int> skup{3, 5, 1, 4, 3, 7};


for(int x : skup) std::cout << x << " ";
std::multiset<int> multiskup{3, 5, 1, 4, 3, 7};
for(int x : multiskup) std::cout << x << " ";

Predavanje 7_b
Akademska godina 2013/14

// Ispis 1 3 4 5 7
// Ispis 1 3 3 4 5 7

Slino, da smo se kroz skup (multiskup) kretali uz pomo iteratora za skup (multiskup), na njihove
elemente bismo nailazili u sortiranom poretku (pri tome e funkcija begin dati iterator koji pokazuje
na najmanji element skupa).
Skup na poetku moe biti prazan, ali mu poetnu veliinu (za razliku od vektora, dekova i listi) ne
moemo zadati. U skupove se novi elementi dodaju iskljuivo pomou funkcije insert, pri emu se
navodi samo element koji ubacujemo, ali ne i njegova pozicija (pozicija se dodue moe zadati preko
iteratora, kao recimo kod vektora ili liste, ali ona ne utie na mjesto gdje se zaista element ubacuje, nego
samo moe pomoi da element doe bre na svoju utvrenu poziciju u sortiranom redoslijedu). Isto tako,
funkciji erase se kao parametar umjesto iteratora koji pokazuje na element koji se brie moe
direktno proslijediti i vrijednost elementa koji se brie (tada e se taj element potraiti i izbristi ako se
nalazi u skupu). Stoga se ranije navedeni primjer za unos niza brojeva i njihovo smjetanje u sortirani
poredak mogao pomou skupa realizirati prosto ovako (stavimo li set umjesto multiset,
eventualno ubacivanje elemenata koji su ve ubaeni bie ignorirano):
std::multiset<int> multiskup;
for(;;) {
int x;
std::cin >> x;
if(x == 0) break;
skup.insert(x);
}
for(auto x : multiskup) std::cout << x << " ";

Zbog naina na koji su elementi skupova meusobno povezani u memoriji (svaki element sadri interni
pokaziva na po jedan element manji i vei od njega, inei strukturu koja se u teoriji podataka naziva
balansirano binarno stablo), ne samo pronalaenje elemenata skupa nego i umetanje novih elemenata i
njihovo uklanjanje vri se vrlo efikasno. Skupove treba koristiti onda kada nam tana pozicija elemenata
u kolekciji, ali imamo potrebu da ih brzo umeemo, briemo i pronalazimo (za razliku od skupova, lista
ne dozvoljava brzo pronalaenje elemenata, jer se ne moe koristiti binarna pretraga).
Za pronalaenje pozicije nekog elementa moe se koristiti funkcija find nad skupom i ona kao
rezultat daje iterator koji pokazuje na element ija se vrijednost zadaje kao parametar (na primjer,
skup.find(5) dae kao rezultat iterator koji pokazuje na poziciju gdje se element nalazi u skupu), ili
iterator iza kraja skupa ukoliko element nije naen. Funkcija count primijenjena nad skupom daje
kao rezultat koliko puta se u skupu ili multiskupu pojavljuje element ija se vrijednost zadaje kao
parametar (s obzirom da skupovi ne doputaju viestruko ponavljanje pohranjenih, elemenata za njih ova
funkcija moe vratiti samo 0 ili 1, dok za multiskupove ona moe vratiti i broj vei od 1). Alternativno,
za istu stvar mogu se koristiti i klasine istoimene funkcije iz biblioteke algorithm (primijenjene na
odgovarajue iteratore za skup ili multiskup), ali ovako je definitivno jednostavnije.
Jedno od ogranienja skupova je to elementi koji se stavljaju u skupove moraju biti takvi da za njih
mora biti definiran poredak pomou operatora <. To recimo iskljuuje mogunost kreiranja skupova
iji su elementi recimo kompleksni brojevi. Postoji nain da se definiraju i skupovi zadavanjem vlastitog
kriterija koji definira poredak, ali to je malo delikatnija tema u koju se neemo uputati. U C++11
uvedena je i varijanta skupova i multiskupova kojima odgovaraju tipovi unordered set i
unordered multiset a definirani su u biblioteci unordered set. Za razliku od klasinih
skupova i multiskupova, za njih ne mora biti definiran poredak i ne postoji nikakva garancija o poretku u
kojem e se njihovi elementi uvati (tako da prolazak kroz njih bilo iteratorima ili rangovskom
for-petljom ne garantira nikakav odreen poredak kojim e se nailaziti na pojedine elemente). Prednost
ove varijante skupova to je rad s njima (umetanje, brisanje i pretraga) tipino bri nego kod klasinih
skupova, jer se zasnivaju na jednoj naprednoj strukturi podataka za brzi pristup podacima poznatu u
teoriji podataka kao rasprena tablica (engl. hash table). Ipak, upotreba ovih skupova zahtijeva da za tip
podataka koji se u njih stavlja bude definirana tzv. funkcija rasprenja (engl. hash function) ili kratko
5

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

raspriva (engl. hasher). Ona je podrazumijevano definirana za veinu standardnih tipova, a postoji i
mogunost da programer sam definira raspriva za neki tip podataka, u ta se neemo uputati.
Za sve tipove iteratora, biblioteka iterator definira i dvije korisne funkcije advance i
distance. Konstrukcija std::advance(it, n) logiki radi istu stvar kao i it += n, samo to
je podrana i za one vrste iteratora za koje operator += nije definiran (ukoliko jeste, ona prosto izvri
it += n). U takvim sluajevima, ova funkcija prosto poziva u petlji operaciju ++ nad iteratorom
it onoliko puta koliko je potrebno da se iterator pomjeri n mjesta unaprijed (ili operaciju
ukoliko je n negativan, a iterator je barem dvosmjerni). Jasno je da ovo nije efikasno, ali je svakako
jednostavnije od runog pisanja petlje. Slino, konstrukcija std::distance(it1, it2) logiki
vraa isti rezultat kao i it2 it1, samo to radi i za one vrste iteratora koji ne podravaju operaciju
-. U takvim sluajevima, ova funkcija prosto u petlji broji koliko puta moe poveati it1 dok on ne
postane jednak it2 i vrati rezultat brojanja kao svoj rezultat.
Kroz konstantne vektore, liste i ostale kontejnerske tipove (pri emu se parametri funkcije koji su
deklarirani kao reference na konstantne objekte, to se esto radi, takoer tretiraju kao konstantni) ne
moe se kretati obinim iteratorima, nego samo iteratorima za konstantne kontejnere. Takvi iteratori se
deklariraju slino kao obini, samo se koristi const_iterator umjesto samo iterator. Na
primjer, sljedea deklaracija deklarira iterator za konstantni vektor cijelih brojeva:
std::vector<int>::const_iterator it;

Rezultat dereferenciranja iteratora za konstantne kontejnere je konstantan objekat, pa mu se ne moe


nita dodjeljivati. Isto tako, iteratoru za konstantne kontejnere mogue je dodijeliti obini iterator
(odgovarajueg tipa), ali obrnuto ne. Pri tome, funkcije begin i end primijenjene na konstantne
kontejnere dae kao rezultat iterator za konstantni kontejner umjesto obinog iteratora, o emu treba
voditi rauna.
Zanimljiva vrsta iteratora su obrnuti iteratori (engl. reverse iterators). Ovi iteratori gledaju
kontejner kroz koji se kreu u ogledalu, odnosno posmatraju kao da je kraj kontejnera poetak, a
poetak kraj. Deklariraju se tako to se umjesto iterator pie reverse_iterator (postoji i
const_reverse_iterator za konstantne objekte). Funkcije rbegin i rend primijenjene na
neki kontejner daju kao rezultat obrnute iteratore koji pokazuju respektivno na kraj i tano ispred
poetka kontejnera. Ove iteratore operator ++ pomjera ka poetku, a ka kraju (bukvalno sve za
ove iteratore je izvrnuto kao u ogledalu, tanije oni kontejner vide u ogledalu i ono to je kraj kontejnera
oni misle da je njegov poetak). Sljedei primjer e ispisati elemente vektora unazad (auto
deklaracija nas je spasila da ne piemo std::vector<int>::reverse_iterator):
std::vector<int> v{3, 5, 2, 1, 6};
for(auto it = v.rbegin(); it != v.rend(); it++)
std::cout << *it++ << " ";

// Ispis 6 1 2 5 3

Obrnuti iteratori vjerovatno djeluju pomalo egzotino, ali oni mogu imati vrlo interesantne
primjene. Slijedi jednostavan primjer koji koristi i obine i obrnute iteratore te funkciju equal iz
biblioteke algorithm da jednim pozivom funkcije utvrdi da li je zadani string s palindrom ili ne
(na prvo gledanje, malo je nezgodno shvatiti kako ovaj primjer radi, ali stvari se ubrzo razbistre):
std::string s("KALABALAK");
if(std::equal(s.begin(), s.end(), s.rbegin()))
std::cout << "Palindrom...";
else std::cout << "Nije palindrom...";

// Palindrom...

Biblioteka iterator definira neke vane vrste iteratora i funkcije za njihovo kreiranje. Posebno
su interesantni tzv. umetai (engl. inserters). Umetai su vrsta striktno izlaznih iteratora koji kada se
pomou njih upisuje element u neki kontejner, svi elementi koji slijede iza elementa na koji pokazuje
iterator se pomjeraju za jedno mjesto unaprijed (odnosno novi element se umee a ne prepisuje preko
postojeih elemenata), pri emu se veliina kontejnera prilikom svakog umetanja automatski poveava
za jedinicu. Stoga se umetai mogu koristiti jedino sa kontejnerima koji mogu mijenjati svoju veliinu.

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

Kao primjer primjene, pretpostavimo da elimo elemente nekog deka prepisati u vektor koji je na
poetku prazan. Naivni pokuaj da to uradimo isjekom poput sljedeeg:
std::deque<int> d{3, 5, 2, 8, 4};
std::vector<int> v;
std::copy(d.begin(), d.end(), v.begin());

// NEISPRAVNO!!!

vrlo vjerovatno e dovesti do kraha. Naime, sve kopirajue funkcije poput copy podrazumijevaju da u
odreditu ima dovoljno prostora da prihvati elemente koji se kopiraju, to ovdje nije ispunjeno. Naravno,
problem bi bio rijeen da smo definirali da vektor v ima 5 elemenata. Meutim, pogledajmo i
alternativno rjeenje koje koristi umetae:
std::deque<int> d{3, 5, 2, 8, 4};
std::vector<int> v;
std::copy(d.begin(), d.end(), std::back_inserter(v));

// OK!

Funkcija back_inserter trai kao parametar neki kontejner, a vraa umeta koji pokazuje
tano iza kraja kontejnera. Svako pisanje pomou tog umetaa dodaje element na kraj kontejnera,
poveavajui njegovu veliinu za 1. Tipovi iteratora koje vraa funkcija back_inserter imaju
uasna imena (poput std::back_insert_iterator<std::vector<int>> u ovom konkretnom
primjeru), ali ona su tipino nebitna, jer se rezultati ovih funkcija obino direktno prosljeuju drugim
funkcijama kao parametri. Slina je funkcija front_inserter koja vraa umeta koji pokazuje na
poetak kontejnera, tako da vri umetanje na poetak kontejnera. Meutim, ovaj umeta je definiran
samo za kontejnere koji podravaju funkciju push_front (recimo, lista ili dek). Postoji i univerzalni
umeta koji se moe koristiti za umetanje na proizvoljno mjesto. On se dobija pozivom funkcije
inserter. Ta funkcija kao parametre zahtijeva kontejner i obini iterator koji pokazuje na mjesto
gdje se vri umetanje, a vraa kao rezultat odgovarajui umeta. Jedino to se za kontejner zahtijeva je
da podrava funkciju insert. Slijedi primjer primjene, nakon kojeg e vektor v sadravati redom
elemente 2, 6, 3, 5, 2, 8, 4, 1 i 7:
std::deque<int> d{3, 5, 2, 8, 4};
std::vector<int> v{2, 6, 1, 7};
std::copy(d.begin(), d.end(), std::inserter(v, v.begin() + 2));

Iteratori koji su samo ulazni odnosno samo izlazni obino su iteratori koji se kreu kroz datoteke, o
emu neemo govoriti jer nismo jo govorili o radu sa datotekama. Interesantno je da se iteratori ak
mogu kretati i kroz ureaje kao to su recimo ekran (takav iterator je striktno izlazni iterator) ili
tastatura (takav iterator je striktno ulazni iterator). Recimo, sljedei primjer djeluje prilino okantno, jer
ispisuje brojeve 3, 2 i 7 na ekran (razdvojene razmacima) iako se u primjeru nigdje ne spominje naredba
za ispis (ovaj primjer zahtijeva biblioteku iterator jer ona definira tip ostream_iterator koji
je namijenjen za kretanje kroz izlazne tokove):
std::ostream_iterator<int> it(std::cout, " ");
*it++ = 3; *it++ = 2; *it++ = 7;

Ovdje smo kreirali iterator it koji se kree po ekranu (to je odreeno navoenjem objekta toka
cout prilikom inicijalizacije) u smislu da svaki upis na mjesto gdje on pokazuje i njegovo
pomjeranje unaprijed zapravo dovodi do ispisa podatka koji se tamo upisuje uz pratei tekst (za
razdvajanje) koji je zadan kao drugi parametar pri konstrukciji iteratora. Ovo djeluje posve uvrnuto, ali
ne treba zaboraviti da se ovakvi iteratori mogu koristiti gdje god se moe koristiti bilo koji izlazni
iterator. Recimo, sljedei primjer koristi funkciju remove_copy i ovakav iterator da ispie na ekran
sve elemente vektora v osim onih koji su jednaki nuli, svaki u posebnom redu:
std::remove_copy(v.begin(), v.end(),
std::ostream_iterator(std::cout, "\n"), 0);

Veina funkcija iz biblioteke algorithm pravljene su tako da se u njihovoj izvedbi koristi samo
najminimalniji mogui skup operacija nad iteratorima, tako da veina njih radi sa bilo kojom vrstom
iteratora (stoga se mogu koristiti za veliki broj razliitih kontejnera). Manji broj funkcija koje zahtijevaju
da se kroz kontejner prolazi i unazad, zahtijevaju dvosmjerne iteratore (takve su, na primjer, funkcije
7

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

copy backward, reverse i reverse copy). Rijetke su funkcije koje trae da iteratori budu sa
direktnim pristupom, ali takve su naalost sve funkcije za sortiranje. Stoga se funkcije poput sort iz
biblioteke algorithm ne mogu primijeniti na sortiranje recimo listi. Sreom, tip podataka list
posjeduje i svoju vlastitu funkciju za sortiranje koja se isto zove sort, a koristi specijalan algoritam
prilagoen upravo za sortiranje listi (neznatno sporiji od opeg algoritma). Ova funkcija se primjenjuje
direktno nad objektima tipa liste. Recimo, ako je lista neka lista, tada e poziv lista.sort()
obaviti sortiranje liste u rastui poredak. Opcionalno se ovoj funkciji moe poslati kao parametar
funkcija kriterija po kojem se obavlja sortiranje. Meutim, treba obratiti panju da je sortiranje listi
pomou ove funkcije po principu sve ili nita, odnosno nije mogue sortirati samo dio liste, kao to je
mogue postii opom funkcijom sort.
Bitno je spomenuti da u jeziku C++ postoje i kontejnerski tipovi podataka koji ne samo da ne
podravaju indeksiranje, nego ak ne podravaju ni iteratore. Stoga je pristup podacima u takvim
kontejnerima vrlo ogranien. Postoje tri tipa takvih kontejnera u jeziku C++: stek (engl. stack), red (engl.
queue) i red sa prioritetom (engl. priority queue). Imena tih tipova su redom stack, queue i
priority queue. Prvi od njih se nalazi u istoimenoj biblioteci, dok se drugi i trei nalaze u
biblioteci queue. Ovi kontejneri su vrlo znaajni za praksu, jer se mnogi poznati algoritmi oslanjaju
na njihovu upotrebu. U sva tri kontejnera novi elementi se dodaju pomou funkcije push, iji je
parametar element koji se dodaje, dok se u svakom trenutku moe pristupiti samo elementu koji je na
vrhu kontejnera, i to se vri pozivom funkcije top bez parametara nad kontejnerom. Meutim,
razlika izmeu ovih kontejnera je ta znai na vrhu. Stek je zasnovan na principu zadnji uao, prvi
izaao ili LIFO (engl. Last In First Out) principu, po kojem je element na vrhu uvijek onaj koji je
posljednji stavljen u stek. Red je zasnovan na prvi uao, prvi izaao ili FIFO (engl. First In First Out)
principu, po kojem je element na vrhu uvijek onaj koji je prvi stavljen u red. Konano, red sa prioritetom
je zasnovan na principu najbolji prvi izlazi ili BFO (engl. Best First Out) principu, po kojem je
element na vrhu uvijek onaj koji je najbolji meu elementima koji su u njega stavljeni. Podrazumijevano
najbolji znai najvei, iako se kriterij dobrote moe zadavati, ali u to neemo ulaziti. Konano, ovi
kontejneri podravaju i funkciju pop (takoer bez parametara) koja uklanja element sa vrha kontejnera
tako da element koji je bio sljedei ispod vrha postaje novi vrh. Pored navedenih operacija, ovi
kontejneri podravaju i funkciju size, ali se u radu sa njima mnogo ee koristi funkcija empty
koja daje logiku vrijednost tano ako i samo ako je kontejner prazan, inae daje netano (ova
funkcija je podrana i za sve druge vrste kontejnera). Slijedi jednostavan primjer koji ilustrira principe
rada ova tri kontejnera:
std::stack<int> s;
s.push(3); s.push(2); s.push(5); s.push(1); s.push(6);
while(!s.empty()) {
std::cout << s.top() << " ";
// Ispii element sa vrha...
s.pop();
// Ukloni ga...
}

Ovaj primjer e ispisati redom brojeve 6, 1, 5, 2 i 3, odnosno elementi se vade obrnutim redom od
redoslijeda umetanja. Ukoliko zamijenimo stack sa queue, ispis e biti 3, 2, 5, 1 i 6, odnosno
elementi se vade u redoslijedu umetanja. Konano, upotrijebimo li priority_queue, ispis e biti 6,
5, 3, 2 i 1, jer prioritet pri vaenju uvijek ima najvei element.
Sada je potrebno proiriti znanja koja smo do sada stekli o anonimnim (lambda) funkcijama. U
dosadanjim primjerima njihove upotrebe nismo imali situaciju da su lambda funkcije trebale pristupati
ikakvim promjenljivim osim svojim parametrima i lokalnim promjenljivim definiranim unutar njihovog
tijela. Meutim, pokuaj da unutar lambda funkcije pristupimo nekoj promjenljivoj definiranoj unutar
bloka koji okruuje tu lambda funkciju (a ne unutar nje same) nee uspjeti. Na primjer, sljedei naivni
pokuaj da pomou funkcije count_if i lambda funkcije prebrojimo koliko u nekom vektoru v
cijelih brojeva ima brojeva koji su vei od nekog broja unesenog sa tastature nee se kompajlirati:
std::cout << "Unesi broj: ";
int broj;
std::cin >> broj;
std::cout << "Broj brojeva veih od " << broj << " iznosi "
<< std::count_if(v.begin(), v.end(), [](int x) { return x > broj; });

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

Problem je uzrokovan injenicom to se navedena lambda funkcija prosljeuje kao parametar


funkciji count_if unutar koje se ona treba i izvravati, a unutar funkcije count_if promjenljiva
broj se ne vidi. Da bismo rijeili problem, lambda funkcija treba da nekako zarobi (engl. capture)
promjenljivu broj i da je povue sa sobom u funkciju count_if. To se postie tako to se unutar
uglastih zagrada u definiciji lambda funkcije navede spisak zarobljavanja (engl. capture list) koji
predstavlja spisak svih promjenljivih koje lambda funkcija treba da zarobi iz svog okruenja, meusobno
razdvojenih zarezima ako ih ima vie. U konkretnom primjeru, jedina promjenljiva koju treba zarobiti
je broj, tako da bi prethodnu definiciju lambda funkcije trebalo prepraviti u sljedeu da prethodna
konstrukcija proradi:
[broj](int x) { return x > broj; }

Zarobljene vanjske promjenljive unutar lambda funkcije tretiraju se kao konstante, odnosno unutar
njihovog tijela vrijednosti zarobljenih promjenljivih se ne mogu mijenjati. Ukoliko elimo da se
vrijednost zarobljene promjenljive moe mijenjati unutar lambda funkcije, ispred imena promjenljive
koja se zarobljava treba staviti oznaku reference & i to se naziva zarobljavanje po referenci (engl.
capture by reference), za razliku od klasinog zarobljavanja po vrijednosti (engl. capture by value).
Tehniki, u tom sluaju zaista se zarobljava referenca, a ne sama promjenljiva. Sljedei primjer koristi
funkciju for_each i lambda funkciju koja zarobljava promjenljivu po referenci da na neobian nain
nae sumu svih elemenata u vektoru v:
std::vector<int> v{3, 5, 2, 8, 6};
int suma(0);
std::for_each(v.begin(), v.end(), [&suma](int x) { suma += x; });
std::cout << "Suma je: " << suma << std::endl;

Vidjeemo kasnije da funkcija moe vratiti kao rezultat lambda funkciju. U tom sluaju moe biti
opasno ukoliko vraena lambda funkcija zarobi po referenci neku lokalnu promjenljivu definiranu u
funkciji koja je vratila lambda funkciju kao rezultat, jer e ta lokalna promjenljiva prestati postojati im
se ta funkcija zavri (na taj nain bismo dobili viseu referencu). Ipak, ovo su dosta rijetke situacije.
Ukoliko elimo zarobiti sve promjenljive iz okruenja koje se spominju unutar lambda funkcije po
vrijednosti, umjesto kompletnog spiska promjenljivih, dovoljno je u uglaste zagrade pisati samo znak
= (tj. pisati [=]). Slino, stavljanje samo znaka & u uglaste zagrade (tj. pisanje [&]) zarobljava
sve promjenljive iz okruenja po referenci. Mogue su i osim varijante zarobljavanja, poput
[&, a, b] koja znai zarobi sve po referenci, osim promjenljivih a i b koje se zarobljavaju po
vrijednosti, te [=, &a, &b] koja znai zarobi sve po vrijednosti, osim promjenljivih a i b koje se
zarobljavaju po referenci.
Lambda funkcije koje zarobljavaju promjenljive iz svog okruenja nemaju status klasinih funkcija,
jer one sa sobom nose i dopunske informacije, vrijednosti ili adrese zarobljenih promjenljivih (ovisno je
li zarobljavanje po vrijednosti ili referenci). U teoriji programiranja, takve funkcije koje nose sa sobom
vrijednosti zarobljene iz svog okruenja nazivaju se lambda zatvorenja (engl. lambda closures) ili nekad
samo kratko zatvorenja. Upravo iz tog razloga, funkcije koje primaju kao svoj parametar obine funkcije
ne mogu primiti lambda zatvorenja kao parametre, niti se pokaziva na obinu funkciju moe postaviti
da pokazuje na lambda zatvorenje. Na primjer, neka imamo sljedeu funkciju nazvanu Tabeliraj
koja ispisuje tablicu vrijednosti funkcije koja joj se zadaje kao prvi parametar za opseg cjelobrojnih
argumenata koji se daju kao drugi i trei parametar:
void Tabeliraj(int f(int), int x_min, int x_max) {
for(int x = x_min; x <= x_max; x++)
std::cout << std::setw(10) << x
<< std::setw(10) << f(x) << std::endl;
}

Slijede primjeri dva poziva funkcije Tabeliraj kojoj se alje lambda funkcija kao prvi parametar, od
kojih je prvi legalan a drugi nije (s obzirom da u drugom sluaju imamo lambda zatvorenje):
int a(1);
Tabeliraj([](int x) { return x * x + 1; }, 0, 10);
Tabeliraj([a](int x) { return x * x + a; }, 0, 10);

// OK!
// NIJE OK!

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

Postoji vie naina da se rijei ovaj problem. Jedan od naina je koritenje jednog tipa podataka koji
poopava pokazivae na funkcije, a ima runo ime polimorfni funkcijski omota (engl. polymorphic
function wrapper). Za razliku od obinih pokazivaa na funkcije, ovaj tip moe prihvatiti ne samo adresu
funkcije, nego i pratee podatke koje je ona zarobila. Tanije, u promjenljivim ovog tipa mogu se uvati
ne samo obine funkcije, obine lambda funkcije i lambda zatvorenja, nego svi drugi objekti koji se
sintaksno ponaaju poput funkcija, odnosno koji se mogu pozivati kao da se radi o klasinim funkcijama
(kasnije emo vidjeti da se takva vrsta objekata nazivaju funktori). Ovaj tip definiran je u biblioteci
functional, a ime mu je function nakon ega u iljastim zagradama slijedi odgovarajui prototip
funkcije, samo bez imena funkcije i parametara). Recimo, ukoliko bismo funkciju Tabeliraj
prepravili da izgleda ovako:
void Tabeliraj(std::function<int(int)> f, int x_min, int x_max) {
// Tijelo funkcije ostaje isto...
}

prethodni pozivi bi radili ispravno. Tanije, funkciji Tabeliraj bismo kao prvi parametar mogli
proslijediti bilo ta to se moe pozvati sa cjelobrojnim parametrom i to vraa rezultat koji se moe
pridruiti cjelobrojnoj promjenljivoj (to ukljuuje recimo i funkciju koja prima double kao
parametar, a ne int, to u prvoj verziji funkcije Tabeliraj nije bilo mogue).
Kao to emo uskoro vidjeti, polimorfni funkcijski omotai mogu biti jako korisni. Meutim, u
ovom konkretnom primjeru, mogli smo lake i bolje proi i bez njih. Naime, bilo bi dovoljno staviti da je
funkcija Tabeliraj generika, pri emu se njen prvi parametar odreuje potpunom dedukcijom, to
bi izgledalo recimo ovako:
template <typename FunkcijskiTip>
void Tabeliraj(FunkcijskiTip f, int x_min, int x_max) {
// Tijelo funkcije ostaje isto...
}

Jasno je da bismo sada funkciji Tabeliraj mogli kao prvi parametar poslati doslovno bilo ta, a
to bi se uspjeno kompajliralo kadgod se taj prvi parametar moe pozvati poput funkcije koja prima cijeli
broj i vraa cijeli broj. Zapravo, ovakva varijanta se upravo i preporuuje kad god se funkcijama alju
druge funkcije i stvari nalik funkcije kao parametri, s obzirom da kada se koriste generike funkcije,
kompajler moe jako mnogo stvari da zakljui jo za vrijeme kompajliranja i da mnogo bolje optimizira
prevedeni kd (ak i u odnosu na varijantu kada se prenos funkcija kao parametara koriste klasini
pokazivai na funkcije). Druga krajnost su polimorfni funkcijski omotai, kod kojih se odluka o tome ta
oni tano predstavljaju donosi u toku izvravanja programa, odnosno u trenutku poziva onoga to oni
predstavljaju, ime se prilino gubi na efikasnosti.
Treba naglasiti da kada se kao parametri u generike funkcije prenose druge funkcije, poeljno je za
parametar koji predstavlja funkciju uvijek koristiti potpunu dedukciju, a ne djeliminu. Naime,
pretpostavimo da smo recimo funkciju Tabeliraj napisali ovako, u kojem parametar f koristi
djeliminu dedukciju tipa:
template <typename ArgTip>
void Tabeliraj(ArgTip f(ArgTip), int x_min, int x_max) {
// Tijelo funkcije ostaje isto...
}

Ovakvoj funkciji ne samo da ne bismo mogli kao prvi parametar poslati lambda zatvorenje, nego ak ni
obinu lambda funkciju (koja nita ne zarobljava iz okruenja). Naime, lambda funkcije, ak i kad nita
ne zarobljavaju, nisu po tipu posve identine obinim funkcijama, mada se mogu koristiti gotovo u svim
kontekstima u kojima se mogu koristiti klasine funkcije (slino kao to tip char nije ba posve isti
kao tip int, mada se moe koristiti praktino u svim kontekstima u kojima i tip int). Meutim, kod
generikih funkcija, da bi mehanizam dedukcije uspjeno radio, on pokuava da uspostavi potpuno
slaganje izmeu tipa onoga to je funkciji poslano i onoga to pie u zaglavlju, to u ovom sluaju nee
uspjeti. Zbog toga se upotreba djelimine dedukcije za funkcijske parametre kod generikih funkcija
izbjegava, jer znatno ograniava njihovu upotrebnu vrijednost.

10

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

Glavna primjena polimorfnih funkcijskih omotaa nije toliko da slue kao parametri funkcijama,
nego da omogue pohranjivanje funkcija i slinih objekata u promjenljivim, kao i omoguavanje da
funkcija vrati funkciju kao rezultat (za tu svrhu, povratni tip funkcije treba da bude upravo funkcijski
omota, odnosno function). Ovo na prvi pogled izgleda malo udno, ali bie ilustrirano na nekoliko
primjera. Primjeri su pomalo apstraktni, s obzirom da trae potpuno razumijevanje razlike izmeu pojma
funkcija i vrijednost funkcije za konkretne vrijednosti argumenta, ali je njihovo razumijevanje
definitivno vrijedno truda. Prvi primjer definira funkciju nazvanu AproksimacijaIzvoda koja kao
parametre prima neku funkciju f koja prima realan broj i vraa realan rezultat, te realni broj (ovaj
parametar ima podrazumijevanu vrijednost 105). Ova funkcija kao rezultat vraa novu funkciju koja
predstavlja aproksimaciju izvoda funkcije f prema formuli f ' (x) ( f (x + ) f (x)) / . Treba dobro
obratiti panju da ova funkcija vraa funkciju (koja se dakle moe primijeniti na neki proizvoljan
argument), a ne konkretnu vrijednost aproksimacije izvoda u nekoj konkretnoj taki:
std::function<double(double)> AproksimacijaIzvoda(
double f(double), double eps = 1e-5) {
return [f, eps](double x) { return (f(x + eps) f(x)) / eps; };
}

Ova funkcija se moe upotrijebiti recimo ovako (ovdje se moe iskoristiti auto deklaracija, umjesto
std::function<double(double)> ali smo namjerno imenovali tip da bude jasnije o kakvom se
tipu promjenljive radi:
std::function<double(double)> fprim(AproksimacijaIzvoda(std::sin));
std::cout << fprim(1);

Ovaj primjer definira novu funkciju fprim koja predstavlja aproksimaciju izvoda standardne funkcije
sin, i ispisuje rezultat primjene te nove funkcije za vrijednost argumenta 1. Rezultat funkcije
AproksimacijaIzvoda (koji je nova funkcija) mogao se i direktno primijeniti na neki argument,
kao u sljedeoj konstrukciji:
std::cout << AproksimacijaIzvoda(std::sin)(1);

Drugi primjer definira funkciju nazvanu Kompozicija. Ova funkcija prima dvije funkcije (ili
neto tome slino) f i g koje se mogu pozvati sa cjelobrojnim parametrom i daju rezultat koji se moe
pridruiti cjelobrojnoj promjenljivoj, a vraa kao rezultat novu funkciju, nazovimo je h, koja primjenjena
na neki argument x daje isti rezultat kao kompozicija funkcija f i g, odnosno h(x) = g( f (x)):
std::function<int(int)> Kompozicija(std::function<int(int)> f,
std::function<int(int)> g) {
return [f, g](int x) { return g(f(x)); };
}

Sad, ukoliko su date neke konkretne funkcije fun1 i fun2 koje prihvataju cjelobrojni parametar i
daju cjelobrojni rezultat, ova funkcija se moe primijeniti recimo ovako:
auto fun3(Kompozicija(fun1, fun2));
std::cout << fun3(5);

// isti rezultat kao fun2(fun1(5))

Ili, ak i posve direktno ovako (mada ovdje to nije od pretjerane koristi):


std::cout << Kompozicija(fun1, fun2)(5);

Konano, posljednji primjer definira funkciju nazvanu FiksirajPrvi. Ova funkcija prima kao
prvi parametar neku funkciju f sa dva cjelobrojna argumenta (nazovimo ih x i y), kao i cijeli broj x0, a
kao rezultat vraa novu funkciju (nazovimo je g) sa jednim cjelobrojnim argumentom takvu da je
g(y) = f (x0, y). Dakle, rezultat je nova funkcija sa jednim parametrom manje, a koja se ponaa poput
polazne funkcije u kojoj je prvi parametar fiksiran na zadanu vrijednost x0 (funkcija donekle slinog
dejstva ve postoji u biblioteci functional pod imenom bind1st);

11

Dr. eljko Juri: Tehnike programiranja /kroz programski jezik C++/


Radna skripta za kurs Tehnike programiranja na Elektrotehnikom fakultetu u Sarajevu

Predavanje 7_b
Akademska godina 2013/14

std::function<int(int)> FiksirajPrvi(int f(int, int), int x0) {


return [f, x0](int y) { return f(x0, y); };
}

Sad, ukoliko je fun neka funkcija dva cjelobrojna argumenta, poziv ove funkcije moe izgledati
recimo ovako:
auto fun2(FiksirajPrvi(fun, 5));
std::cout << fun2(10);

// isti rezultat kao fun(5, 10)

Glavna primjena ovakve i slinih funkcija je to one mogu adaptirati funkciju od recimo dva
argumenta i pretvoriti je u funkciju od jednog argumenta (fiksirajui joj prvi parametar) koja se dalje
moe poslati kao parametar nekoj funkciji koja oekuje funkciju od jednog argumenta (poput recimo
funkcije kriterija za funkciju count_if).
Kao to je ve reeno, izloeni primjeri su, mada jednostavni, dosta apstraktni i trae potpuno
drugaije gledanje na programiranje u odnosu na konvencionalno. Meutim, upravo takve konstrukcije
lee u osnovi jedne tehnike programiranja koja u posljednje vrijeme sve vie dobija na znaaju, koja je
poznata pod nazivom funkcionalno programiranje.

12

You might also like