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

VANREDNI POPRAVNI II PARCIJALNI ISPIT

IZ PREDMETA “TEHNIKE PROGRAMIRANJA”


Zadatak 1 (7,5 poena)
Na tutorijalu iz TP postavljen je zadatak koji u osnovi traži da se razvije polimorfna kontejnerska
klasa “KolekcijaLikova” koja sadrži kolekciju raznih konkretnih geometrijskih likova izvedenih iz
apstraktne bazne klase “Lik” (za početak, tu su tačka i mnogougao). Početno rješenje (sa kratkim
testnim programom) koje je neki student ponudio izgleda ovako:
#include <iostream>
#include <vector>
#include <memory>
class Lik {
public:
enum Boje {Crvena, Zelena, Zuta, Plava, Ljubicasta, Crna, Bijela};
Lik(Boje boja) : boja(boja) {}
Boje DajBoju() const { return boja; }
void Ispisi() const { std::cout << boja; };
private:
Boje boja;
};
class Tacka : public Lik {
double x, y;
public:
Tacka(Lik::Boje boja, double x, double y) : Lik(boja), x(x), y(y) {}
void Ispisi() const { std::cout << "{" << x << "," << y << "}"; }
};
class Mnogougao : public Lik {
int br_tjemena;
double *x, *y;
public:
Mnogougao(Lik::Boje boja, int broj_tjemena) : Lik(boja), br_tjemena(br_tjemena),
x(new double[br_tjemena]), y(new double[br_tjemena]) {}
void PostaviTjeme(int redni_broj, double x, double y) {
Mnogougao::x[redni_broj - 1] = x; Mnogougao::y[redni_broj - 1] = y;
}
void Ispisi() const {
std::cout << "{";
for(int i = 0 ; i < br_tjemena; i++) {
std::cout << "{" << x[i] << "," << y[i] << "}";
if(i != br_tjemena - 1) std::cout << ",";
}
std::cout << "}";
}
};
class KolekcijaLikova {
std::vector<std::shared_ptr<Lik>> likovi;
public:
KolekcijaLikova() {}
void RegistrirajLik(Lik *lik) {
likovi.push_back(std::shared_ptr<Lik>(lik));
}
void IspisiKolekciju() {
for(int i = 0; i < likovi.size(); i++) {
likovi[i]->Ispisi();
std::cout << std::endl;
}
}
};
int main() {
KolekcijaLikova k;
k.RegistrirajLik(new Tacka(Lik::Crvena, 30, 20));
Mnogougao *m(new Mnogougao(Lik::Plava, 4));
m->PostaviTjeme(1, 10, 10); m->PostaviTjeme(2, 10, 50);
m->PostaviTjeme(3, 50, 50); m->PostaviTjeme(4, 50, 10);
k.RegistrirajLik(m);
k.IspisiKolekciju();
return 0;
}
Ovaj program se kompajlira ispravno. Međutim, već su prvi autotestovi detektirali da u ovako
ponuđenom rješenju postoji nekoliko problema:
 Bez obzira što je student htio izbjeći probleme sa upravljanjem memorijom tako što je koristio
vektor pametnih pokazivača za čuvanje pokazivača na elemente kolekcije, ovaj program i dalje
ima curenje memorije, čak i kada ne dolazi problema sa nestašicom memorije (tj. čak i ukoliko ne
dolazi do bacanja izuzetka uslijed nestašice memorije).
 Curenje memorije također nastaje ukoliko u konstruktoru klase “Mnogougao” alokacija niza “x”
uspije, a alokacija niza “y” ne uspije.
 U programu postoji jedna klasa koja je takva da kopiranje i međusobno dodjeljivanje njenih
primjeraka može dovesti do nepredvidljivih rezultata, uključujući i eventualni krah programa.
 Mada se, zahvaljujući pametnim pokazivačima, objekti tipa “KolekcijaLikova” mogu bezbjedno
kopirati i međusobno dodjeljivati, takvo kopiranje je ipak zasnovano na plitkim kopijama
(bezbjednost je ostvarena preko brojanja pristupa, koje rade pametni pokazivači). Zbog ovoga
također padaju neki autotestovi, jer je specifikacija tutorijalskog zadatka bila takva da je jasno da
bilo da se obavlja duboko kopiranje (a i inače, u duhu jezika C++ je da se uvijek podrazumijeva da
se traži duboko kopiranje, ako eksplicitno nije rečeno drugačije).
 Funkcija “PostaviTjeme” u klasi “Mnogougao” prima kao parametar redni broj tjemena, te i
koordinate tjemena. Očekivalo se da ova funkcija baci izuzetak tipa “range_error” uz prateći tekst
“Pogresan redni broj tjemena”, ali napisana funkcija uopće ne provjerava ispravnost rednog broja.
 Mada je glavni testni program zamišljen tako da, nakon što kreira neke geometrijske likove i
registrira ih u kolekciji, ispisuje na ekran osnovne informacije o registriranim objektima, ispis na
ekranu nije u skladu s očekivanjima.
Izvršite neophodne korekcije u programu da ispravite sve uočene greške. Pri tome, ukoliko budete
morali implementirati vlastite postupke za kopiranje primjeraka neke klase, predvidite i optimizirane
verzije za slučaj kada se kopiraju privremeni objekti tipa te klase (recimo, rezultat neke funkcije koja
vraća takve objekte). Što se tiče pogrešnog ispisa koji proizvodi ovaj program, obavezno napišite
kako je izgledao ispis prije izvršene korekcije i kako će izgledati nakon obavljene korekcije.

Rješenje:
Što se tiče curenja memorije, postoje dva razloga. Prvi je što se u konstruktoru klase “Mnogougao”
dinamički alociraju dva niza čije se adrese čuvaju u (običnim) pokazivačima “x” i “y”, a o čijem se
oslobađanju ne brine niko. Rješenje je da se u ovu klasu doda destruktor koji će oslobađati memoriju
koju su zauzela ova dva niza (ko je ispravno uočio i riješio ovaj problem dobija 0.5 poena):
~Mnogougao() { delete[] x; delete[] y; }

Alternativno rješenje je staviti da “x” i “y” budu pametni pokazivači, ali takvo rješenje traži mnogo
prepravki u programu (nije sasvim lako postaviti pametne pokazivače da pokazuju na dinamički
alocirane nizove, zatim pametni pokazivači se ne mogu direktno indeksirati, itd.). Ipak, čak i nakon
ove ispravke ostaje jedan problem. U klasi “KolekcijaLikova” svi objekti se čuvaju preko pametnih
pokazivača koji su deklarirani kao pokazivači na objekte tipa “Lik” (njihov statički tip), mada oni
mogu pokazivati i na objekte nekog tipa izvedenog iz tipa “Lik” (njihov dinamički tip). Međutim,
kasnije kada se budu uništavali ti objekti (iz destruktora pametnih pokazivača), pozivaće se isključivo
destruktor iz klase “Lik”, s obzirom da destruktor u klasi “Lik” nije deklariran kao virtualan (on
zapravo uopće nije ni napisan nego je automatski generiran, a automatski generirani destruktori
nikad nisu virtualni). Drugim riječima, destruktor iz klase “Mnogougao” nikad neće biti pozvan (čak ni
ako pokazivač pokazuje upravo na objekat tog tipa). Da bi se uzelo u obzir faktičko stanje, odnosno
pozvao pravi destruktor u skladu sa dinamičkim tipom pokazivača, destruktor u klasi “Lik” mora biti
virtualan, odnosno moramo imati sljedeću definiciju (ko je ispravno uočio i riješio ovaj problem
dobija 0.5 poena):
virtual ~Lik() {}

Što se tiče curenja memorije koje nastaje ukoliko u konstruktoru klase “Mnogougao” alokacija niza “x”
uspije, a alokacija niza “y” ne uspije, rješenje je da se alokacija niza “y” vrši unutar try-bloka. U slučaju
da alokacija ne uspije, treba obrisati već alocirani niz “x” i proslijediti bačeni izuzetak dalje (ko je
ispravno uočio i riješio ovaj problem, dobija 1 poen):
Mnogougao(Lik::Boje boja, int broj_tjemena) : Lik(boja), br_tjemena(br_tjemena),
x(new double[br_tjemena]) {
try {
y = new double[br_tjemena];
}
catch(...) {
delete[] x; throw;
}
}

Mada nekome može izgledati da su potrebni ugniježdeni try-catch blokovi, to nije istina, jer ako
alokacija niza “x” ne uspije, ništa ne treba brisati. Ispravno je i sljedeće rješenje, čija je dobra stvar to
što se lako generalizira na proizvoljan broj nizova, ali je u osnovi redudantno (ima viška stvari), jer
ako bolje razmislimo, nikad se ne može desiti da u catch-bloku bude alociran niz “y” (da je njegova
alokacija uspjela, nikad ne bismo ni dospjeli u taj blok):
Mnogougao(Lik::Boje boja, int broj_tjemena) : Lik(boja), br_tjemena(br_tjemena),
x(nullptr), y(nullptr) {
try {
x = new double[br_tjemena]; y = new double[br_tjemena];
}
catch(...) {
delete[] x; delete[] y; throw;
}
}

Naravno, postoji još ispravnih rješenja. Što se tiče klase koja je takva da kopiranje i međusobno
dodjeljivanje njenih primjeraka može dovesti do nepredvidljivih rezultata, uključujući i eventualni
krah programa, to je očito klasa “Mnogougao”. Kako ona sadrži pokazivače, podrazumijevani kopirajući
konstruktor kopirao bi samo te pokazivače (plitka kopija), pa bi nakon kopiranja original i kopija
dijelili iste resurse, što može biti fatalno, pogotovo kada se u igru uključe destruktori (kada destruktor
oslobodi resurse koje koristi jedan objekat, bez istih ostaje i drugi objekat koji skupa s njim dijeli te
resurse). Rješenje je dodati u ovu klasu vlastiti kopirajući konstruktor, koji kreira duboku kopiju
klonirajući kompletne nizove na koje “x” i “y” pokazuju, a ne samo pokazivače. Ovdje također trebamo
voditi računa šta raditi ako alokacija prvog niza uspije a drugog ne (slično kao u prethodnom
konstruktoru, i ovo se može riješiti na više načina):
Mnogougao(const Mnogougao &m) : Lik(m.DajBoju()), br_tjemena(m.br_tjemena),
x(new double[m.br_tjemena]) {
try {
y = new double[m.br_tjemena];
std::copy(m.x, m.x + m.br_tjemena, x);
std::copy(m.y, m.y + m.br_tjemena, y);
}
catch(...) {
delete[] x; delete[] y; throw;
}
}

Za slučaj kopiranja privremenih objekata, treba predvidjeti i optimiziranu verziju (tzv. pomjerajući
konstruktor), koja radi po dobro poznatom receptu “pokradi i umrtvi pokradeni objekat”:
Mnogougao(Mnogougao &&m) : Lik(m.DajBoju()), br_tjemena(m.br_tjemena),
x(m.x), y(m.y) {
m.x = nullptr; m.y = nullptr;
}

Gdjegod ima posla za kopirajuće i pomjerajuće konstruktore, tu gotovo uvijek ima posla i za njihove
rođake, kopirajući i pomjerajući operator dodjele. Oboje možemo riješiti “jednim udarcem” koristeći
“instant” rješenje zasnovano na strategiji kopiraj-i-razmijeni. Naravno, možemo ići i “pješke” (tj.
osloboditi postojeće resurse a nakon toga izvršiti kloniranje objekta), ali time ne bismo dobili ništa
efikasnije rješenje u odnosu na “instant” rješenje, pa zato nećemo komplicirati bez potrebe:
Mnogougao &operator =(Mnogougao m) {
std::swap(br_tjemena, m.br_tjemena); std::swap(x, m.x); std::swap(y, m.y);
return *this;
}

Ko god je uočio navedeni problem i implementirao sve što treba, dobija 1.5 poen.
Što se tiče dubokog kopiranja objekata tipa “KolekcijaLikova”, tu nam također trebaju vlastiti
kopirajući/pomjerajući konstruktor, te kopirajući/pomjerajući operator dodjele. Međutim, kako je u
pitanju heterogena kolekcija (imamo elemente različitih tipova), potrebno je podržati polimorfno
kopiranje, što znači da će nam trebati i virtualna metoda za kloniranje objekata (nazvaćemo je recimo
“DajKopiju”). Kako se radi o kolekciji koja čuva pametne pokazivače, najlakše je izvesti da ova metoda
također vraća pametni pokazivač (u suprotnom bismo morali konvertirati obični pokazivač koji bi ona
vratila u pametni prije upisa u kolekciju). Dakle, prvo je potrebno izvršiti ove pripremne radnje:
virtual std::shared_ptr<Lik> DajKopiju() = 0; // u klasu "Lik"
std::shared_ptr<Lik> DajKopiju() override { // u klasu "Tacka"
return std::make_shared<Tacka>(*this);
}
std::shared_ptr<Lik> DajKopiju() override { // u klasu "Mnogougao"
return std::make_shared<Mnogougao>(*this);
}

Nakon toga, pisanje kopirajućeg konstruktora za klasu “KolekcijaLikova” je rutinska stvar, viđena već
mnogo puta dosad. Obratimo pažnju da try-catch blok uopće nije potreban, s obzirom da se u slučaju da
neka alokacija ne uspije, o oslobađanju memorije brinu destruktori pametnih pokazivača (manje-više o
oslobađanju ma čega na šta pokazuju pametni pokazivači nikada se ne moramo brinuti, osim u rijetkim
situacijama kada kreiramo ciklički lanac povezanih čvorova koji sadrže pametne pokazivače). Prikažimo
dva moguća rješenja, oba su krajnje jednostavna.
KolekcijaLikova(const KolekcijaLikova &k) : likovi(k.likovi.size()) {
for(int i = 0; i < k.likovi.size(); i++) likovi[i] = k.likovi[i]->DajKopiju();
}

Drugo rješenje je interesantno jer koristi rangovsku for-petlju:


KolekcijaLikova(const KolekcijaLikova &k) : likovi(k.likovi.size()) {
for(auto lik : k.likovi) likovi.push_back(lik->DajKopiju());
}

Što se tiče optimizirane verzije za kopiranje privremenih objekata (pomjerajući konstruktor), on samo
treba da “pomjeri” atribut “likovi” iz izvorišta u odredište. Međutim, upravo bi to uradio i automatski
generirani pomjerajući konstruktor, pa možemo samo pisati
KolekcijaLikova(KolekcijaLikova &&k) = default;

Konačno, što se tiče kopirajućeg/pomjerajućeg operatora dodjele, ni ovdje nema nikakav razlog da ih
ne izvedemo kao “instant” rješenje tehnikom kopiraj-i-razmijeni:
KolekcijaLikova &operator =(KolekcijaLikova k) {
std::swap(likovi, k.likovi); return *this;
}

Ko god je ispravno i kompletno riješio problem plitkog kopiranja objekata tipa “ KolekcijaLikova”,
dobija 1.8 poena.
Što se tiče testiranja ispravnosti indeksa u funkciji “PostaviTjeme”, iz načina kako je ona napisana
vidljivo je da se indeksacija tjemena vrši od jedinice, a ne od nule. Dakle, legalni su indeksi u opsegu
od 1 do ukupnog broja tjemena uključivo, tako da prepravljena verzija ove funkcije treba da izgleda
ovako (ko je ispravno uočio obje granice i korektno implementirao ovu izmjenu, dobija 0.7 poena):
void PostaviTjeme(int redni_broj, double x, double y) {
if(redni_broj < 1 || redni_broj > br_tjemena)
throw std::range_error("Pogresan redni broj tjemena");
Mnogougao::x[redni_broj - 1] = x; Mnogougao::y[redni_broj - 1] = y;
}

Ostaje još da razmotrimo problem ispisa. Glavni testni program poziva funkciju “IspisiKolekciju”,
koja poziva funkciju “Ispisi” nad svakim pokazivačem u kolekciji. Međutim, kako su ti pokazivači
definirani kao pokazivači na objekte tipa “Lik”, a funkcija “Ispisi” nije proglašena za virtualnu, izbor
funkcije vršiće se u skladu sa deklariranim (statičkim) tipom, odnosno bez obzira na stvarni tip
objekta na koji pokazivač pokazuje, uvijek će se pozivati funkcija “Ispisi” iz klase “Lik”. Kako imamo
registrirana dva objekta (crvenu tačku i plavi mnogougao), a funkcija “Ispisi” iz klase “Lik” ispisuje
samo boju (tačnije cjelobrojni kod bôje, s obzirom da se promjenljive pobrojanog tipa automatski
konvertiraju u brojčane vrijednosti prilikom ispisa), ispis će glasiti
0
3

jer su kodovi za crvenu i plavu boju 0 i 3 respektivno (ko je ispravno detektirao ovaj ispis, dobija 0.5
poena). Međutim, razumno bi bilo očekivati da se poziva ona verzija funkcije “Ispisi” koja odgovara
stvarnom tipu objekta na koji pokazivač pokazuje, odnosno u navedenom primjeru verzija iz klase
“Tacka” odnosno “Mnogougao” respektivno. Kad uzmemo u obzir kako su objekti inicijalizirani i kako
su napisane funkcije “Ispisi”, slijedi da bi ispis zapravo trebao izgledati ovako (ko je ispravno
detektirao ovaj ispis, dobija 0.5 poena):
{30,20}
{{10,10},{10,50},{50,50},{50,10}}

Rješenje je da se funkcija “Ispisi” u baznoj klasi proglasi za virtualnu (ne mora nužno biti čisto
virtualna, odnonso apstraktna), čime će se prilikom poziva uzimati u obzir faktičko stanje (dinamički
tip) a ne samo deklaracije (opcionalno se može dodati i specifikacija “override” u ostalim verzijama
funkcije “Ispisi”). Drugim riječima, u klasi “Lik” treba napraviti sljedeću izmjenu (ko je ispravno
uočio ovaj problem i ponudio rješenje, dobija 0.5 poena):
virtual void Ispisi() const { std::cout << boja; };

Zadatak 2 (8,5 poena)


Nakon što su u prethodnom zadatku obavljene neophodne ispravke pogrešno napisanih dijelova
zadatka (pretpostavimo da jesu, čak iako ih niste ispravili sve), potrebno je implementirati još neke
funkcionalnosti, jer nije podržano sve što se u tutorijalskom zadatku tražilo:
 Potrebna je još jedna verzija metode “PostaviTjeme” koja umjesto i koordinate tjemena prima
objekat tipa “Tacka” kao odgovarajući parametar putem kojeg se zadaje tjeme (pri tome se boja
tačke ignorira), kao i dvije pristupne metode u klasi “Tacka” putem kojih se mogu saznati
informacije o odnosno koodrdinati tačke.
 U napisanom programu, podržane su samo dvije vrste objekata: tačke i mnogouglovi. Međutim,
postavka tutorijalskog zadatka tražila je da se mogu u kolekciju dodatavi i krugovi. Krugovi
trebaju biti opisani sa bojom, centrom i poluprečnikom, pri čemu se centar može zadavati na dva
načina: kao dva realna broja ( i koordinata), ili kao objekat tipa “Tacka”. Treba biti podržano da
se mogu saznavati informacije o centru kruga i poluprečniku, kao i ispis informacija o krugu u
obliku “{{ , }, }”.
 Potrebno je podržati i funkciju “DajPovrsinu” koja omogućava da se za podržane likove može
saznavati informacija o njihovoj površini, pri čemu mora biti podržano da se površina ma kojeg
lika može dobiti bez da se unaprijed zna o kojem se liku tačno radi (pa čak i o nekom novom tipu
lika koji će biti implementiran u budućnosti). Za tačku uzmite da joj je površina 0, za mnogougao
površina se računa prema formuli

1
P ( )+ ( )+ ( )

gdje je broj tjemena, a ( , ) su odgovarajuće koordinate, dok se površina kruga računa po


formuli koju bi trebali da znaju svi koji su došli raditi ovaj ispit (mada je predmetni nastavnik
nedavno imao situaciju na jednoj visokoškolskoj instituciji u Sarajevu da od 20 prisutnih studenata
na ispitu nijedan ne zna formulu za površinu kruga, a od tih 0 studenata polovica je tvrdilo da je
zapravo besmisleno i pamtiti tu formulu jer se ona može izguglati kad god nekom zatreba).
 Potrebno je podržati da se objekti tipa “Krug” mogu množiti sa realnim brojem, tj. da budu legalni
izrazi poput “k * x” i “x * k” gdje je “k” neki objekat tipa “Krug”, a “x” neki realan broj. Kao rezultat
ovih izraza, treba da se dobije novi objekat tipa “Krug” sa istim centrom kao i krug “k”, ali u kojem je
poluprečnik pomnožen sa faktorom “x”. Također je potrebno podržati da se mogu sabirati objekti
tipa “Krug” sa objektima tipa “Tacka”, tj. da budu legalni izrazi poput “k + t” i “t + k” gdje je “k” neki
objekat tipa “Krug”, a “t” neki objekat tipa “Tacka”. Kao rezultat ovih izraza, treba da se dobije novi
objekat tipa “Krug” sa istim poluprečnikom kao i krug “k”, ali čiji je centar transliran za iznos
određen koordinatama tačke “t”. Najzad, treba podržati da izrazi poput “p += q” odnosno “p *= q”
imaju isto značenje kao i izrazi “p = p + q” odnosno “p = p * q” kad god ovi izrazi imaju smisla.
 Pored već napisane funkcije “RegistrirajLik” koja kao parametar prima pokazivač na neki lik,
nakon čega ga upisuje u kolekciju i preuzima vlasništvo nad njim (pretpostavljajući da je on
dinamički alociran), potrebno je dodati još jednu verziju funkcije “RegistrirajLik” koja umjesto
pokazivača prima referencu na neki lik. Za razliku od prethodne funkcije, ova funkcija neće praviti
nikakve pretpostavke kako je objekat kreiran i neće ga uzimati u vlasništvo. Umjesto toga, funkcija
treba da dinamički kreira kopiju objekta koji se registrira i da preuzme vlasništvo nad tako
kreiranom kopijom.
 Konačno, potrebno je u klasi “Mnogougao” omogućiti da se informacije o tjemenu mogu saznavati
ili zadavati navođenjem rednog broja tjemena u uglastim zagradama, tako da ako je “m” neki
objekat tipa “Mnogougao”, izraz “m[3]” treba da predstavlja njegovo treće tjeme, i to u vidu objekta
tipa “Tacka”. Treba omogućiti da se na ovaj način mogu ne samo iščitavati nego i mijenjati
informacije o tjemenima, pri čemu izmjena treba da bude podržana samo za slučaj nekonstantnih
objekata tipa “Mnogogugao”.

Rješenje:
Dodajmo prvo pristupne metode u klasu “Tacka”:
double DajX() const { return x; };
double DajY() const { return y; };

Kada imamo ove pristupne metode, najbolje je novu verziju metode “PostaviTjeme” izvesti njenim
svođenjem na već postojeću verziju, jer je tako najmanje posla (koristimo i činjenicu da postojeća
verzija već testira ispravnost rednog broja tjemena). Dakle, u klasu “Mnogougao” ćemo dodati
void PostaviTjeme(int redni_broj, const Tacka &tjeme) {
PostaviTjeme(redni_broj, tjeme.DajX(), tjeme.DajY());
}

Da nismo izveli svođenje na prethodnu verziju, korektna izvedba mogla bi izgledati i recimo ovako
(kako god, ko je korektno izveo sve što se ovdje traži, dobija 1 poen):
void PostaviTjeme(int redni_broj, const Tacka &tjeme) {
if(redni_broj < 1 || redni_broj > br_tjemena)
throw std::range_error("Pogresan redni broj tjemena");
Mnogougao::x[redni_broj - 1] = tjeme.DajX();
Mnogougao::y[redni_broj - 1] = tjeme.DajY();
}

Što se tiče podrške za krugove, iz postavke problema slijedi da je najkorektnije da dodatni atributi
novonapisane klase “Krug” budu centar (tipa “Tacka”) i poluprečnik, mada je prihvatljivo i rješenje u
kojem su i koordinata centra posebni atributi (iako mora postojati metoda koja vraća centar kao
objekat tipa “Tacka”). Ne smijemo zaboraviti ni na metodu “DajKopiju” koja je neophodna da se
podrži polimorfno kopiranje riješeno u prvom zadatku. Stoga bi implementacija ove klase mogla
izgledati recimo ovako (ko je korektno izveo sve što je ovdje traženo, dobija 1.5 poen):
class Krug : public Lik {
Tacka centar;
double poluprecnik;
public:
Krug(Lik::Boje boja, double xc, double yc, double r) : Lik(boja),
centar(boja, xc, yc), poluprecnik(r) {}
Krug(Lik::Boje boja, const Tacka &centar, double r) : Lik(boja),
centar(centar), poluprecnik(r) {}
Tacka DajCentar() const { return centar; }
double DajPoluprecnik() const { return poluprecnik; }
std::shared_ptr<Lik> DajKopiju() override {
return std::make_shared<Mnogougao>(*this);
}
void Ispisi() const override {
std::cout << "{{" << centar.DajX() << "," << centar.DajY() << "},"
<< poluprecnik << "}";
}
};

Korektna alternativna izvedba, sa razdvojenim atributima za i koordinate centra, mogla bi


izgledati recimo ovako:
class Krug : public Lik {
double xc, yc, poluprecnik;
public:
Krug(Lik::Boje boja, double xc, double yc, double r) : Lik(boja),
xc(xc), yc(yc), poluprecnik(r) {}
Krug(Lik::Boje boja, const Tacka &centar, double r) : Lik(boja),
xc(centar.DajX()), yc(centar.DajY()), poluprecnik(r) {}
Tacka DajCentar() const { return Tacka(boja, xc, yc); }
double DajPoluprecnik() const { return poluprecnik; }
std::shared_ptr<Lik> DajKopiju() override {
return std::make_shared<Mnogougao>(*this);
}
void Ispisi() const override {
std::cout << "{{" << xc << "," << yc << "}," << poluprecnik << "}";
}
};

Što se tiče podrške za računanje površine, da bi se površina ma kojeg lika može dobiti bez da se
unaprijed zna o kojem se liku tačno radi (pa čak i o nekom novom tipu lika koji će biti implementiran
u budućnosti), u baznu klasu “Lik” treba dodati apstraktnu (čisto virtualnu) metodu “DajPovrsinu”
(ko je ispravno uočio ovu činjenicu, dobija 0.3 poena):
virtual double DajPovrsinu() const = 0;

Sad treba dodati i odgovarajuće konkretne metode za računanje površine u izvedenim klasama. U
klasu “Tacka” treba dodati sljedeće (ko je ovo ispravno uradio, dobija 0.2 poena):
double DajPovrsinu() const override { return 0; }

Verzija funkcije “DajPovrsinu” koju treba dodati u klasu “Mnogougao” može izgledati ovako, pri čemu
treba obratiti pažnju da su tjemena smještena u nizove od indeksa 0, a da se inače indeksiraju od 1
svuda drugdje, pa i u ovoj formuli (ko je ispravno implementirao ovu funkciju, dobija 1 poen):
double DajPovrsinu() const override {
double suma(0);
for(int i = 2; i < br_tjemena; i++)
suma += x[0] * (y[i - 1] - y[i] + x[i - 1] * (y[i] - y[0])
+ x[i] * (y[0] - y[i - 1]);
return std::fabs(suma) / 2;
}

Konačno, u klasu “Krug” treba dodati sljedeću funkciju, u kojoj smo iskoristili da je π = 4 ∙ arc tg 1 (ko
je ispravno implementirao ovu funkciju, dobija 0.5 poena):
double DajPovrsinu() const override {
return poluprecnik * poluprecnik * 4 * std::atan(1);
}

Za podršku množenja objekata tipa “Krug” sa realnim brojevima, potrebne su dvije operatorske
funkcije. Prva može biti ili obična funkcija koja prima kao prvi parametar objekat tipa “ Krug” a kao
drugi parametar realni broj, ili funkcija članica klase “Krug” koja prima kao parametar realni broj.
Odlučimo li se za prvu varijantu, njena implementacija mogla bi izgledati recimo ovako:
Krug operator *(const Krug &k, double x) {
return Krug(k.DajBoju(), k.DajCentar(), k.DajPoluprecnik() * x);
}

S obzirom da su svugdje korištene pristupne funkcije, ovu operatorsku funkciju čak nije neophodno
ni proglašavati za prijatelja klase “Krug”. Moguća je i ovakva (kraća) implementacija, u kojoj je bitno
da se prvi parametar prenosi po vrijednosti (s obzirom da nam treba kopija objekta):
Krug operator *(Krug k, double x) { k.poluprecnik *= x; return k; }

Također, ova izvedba zahtijeva da se ova funkcija proglasi za prijatelja klase “Krug”, zbog direktnog
pristupa atributu “poluprecnik”. Stoga unutar klase “Krug” treba dodati deklaraciju
friend Krug operator *(Krug k, double x);
Alternativno, odlučimo li se za izvedbu preko operatorske funkcije članice, tada bismo u klasu “Krug”
trebali dodati odgovarajuću funkciju članicu koja bi mogla izgledati recimo ovako (specifikacija
“const” je neophodna da bi prvi operand mogao biti konstantni objekat):
Krug operator *(double x) const {
return Krug(DajBoju(), centar, poluprecnik * x);
}

Alternativno, moguća je i ovakva izvedba (obratimo pažnju na kreiranje kopije):


Krug operator *(double x) const { Krug k(*this); k.poluprecnik *= x; return k; }

Druga operatorska funkcija mora biti isključivo obična funkcija koja kao prvi parametar prima realni
broj a kao drugi objekat tipa “Krug”. Njena implementacija može biti identična kao i implementacija
prve, a možemo je i prosto svesti na prvu ovako:
inline Krug operator *(double x, const Krug &k) { return k * x; }

U svakom slučaju, ko god je u potpunosti ispravno riješio podršku za množenje objekata tipa “ Krug”
sa realnim brojem i obrnuto, dobija 0.7 poena.

Za podršku sabiranja objekata tipa “Krug” sa objektima tipa “Tacka”, također su potrebne dvije
operatorske funkcije. Prva može biti ili obična funkcija koja prima kao prvi parametar objekat tipa
“Krug” a kao drugi parametar objekat tipa “Tacka”, ili funkcija članica klase “Krug” koja prima kao
parametar objekat tipa “Tacka”. Odlučimo li se za prvu varijantu, njena implementacija mogla bi
izgledati recimo ovako:
Krug operator +(const Krug &k, const Tacka &t) {
return Krug(k.DajBoju(), k.DajCentar().DajX() + t.DajX(),
k.DajCentar().DajY() + t.DajY(), k.DajPoluprecnik());
}

Ni ova operatorska funkcija ne mora nužno biti prijatelj klase “Krug”, jer svuda koristimo pristupne
funkcije. Naravno, moguće su i drugačije implementacije, recimo poput sljedeće (ova implementacija
traži proglašenje funkcije za prijatelja klase “Krug”):
Krug operator +(Krug k, const Tacka &t) {
k.centar = Tacka(k.centar.DajX() + t.DajX(), k.centar.DajY() + t.DajY());
return k;
}

Ova izvedba pretpostavlja da se centar kruga čuva kao atribut tipa “Tacka”. Ukoliko se koordinate
centra čuvaju kao posebni atributi, implementacija bi bila drugačija, i mogla bi izgledati recimo ovako
(također uz neophodnost proglašenja funkcije za prijatelja klase “Krug”):
Krug operator +(Krug k, const Tacka &t) {
k.xc += t.DajX(); k.yc + t.DajY(); return k;
}

Može se primijetiti jedna prednost izvedbi koje ne traže uspostavljanje prijateljstva: one ne ovise od
toga kako je tačno klasa interno implementirana. S druge strane, odlučimo li se za izvedbu preko
operatorske funkcije, tada bismo u klasu “Krug” trebali dodati odgovarajuću funkciju članicu. Njena
tačna izvedba ovisi od toga kako je tačno izvedena klasa “Krug”. Ukoliko je ona izvedena tako da se
centar čuva kao atribut tipa “Tacka”, njena izvedba mogla bi izgledati recimo ovako:
Krug operator +(const Tacka &t) const {
return Krug(DajBoju(), centar.DajX() + t.DajX(),
centar.DajY() + t.DajY(), poluprecnik);
}

Alternativno, možemo ovu funkciju izvesti i ovako (uz kreiranje kopije):


Krug operator +(const Tacka &t) const {
Krug k(*this);
k.centar = Tacka(centar.DajX() + t.DajX(), centar.DajY() + t.DajY());
return k;
}
S druge strane, ukoliko je klasa “Krug” izvedena tako da se koordinate centra čuvaju u odvojenim
atributima, implementacija bi mogla izgledati recimo ovako:
Krug operator +(const Tacka &t) const {
return Krug(DajBoju(), xc + t.DajX(), yc + t.DajY(), poluprecnik);
}

Alternativno, uz kreiranje kopije, možemo ovu funkciju izvesti i ovako:


Krug operator +(const Tacka &t) const {
Krug k(*this); xc += t.DajX(); yc + t.DajY(); return k;
}

Moguće su i brojne druge izvedbe. Međutim, kako god, potrebna nam je i još jedna funkcija koja će
podržati da prvi operand bude tipa “Tacka” a drugi operand tipa “Krug”. Nju također možemo izvesti
na dva načina: kao običnu funkciju čiji je prvi parametar tipa “Tacka” a drugi tipa “Krug”, a također i
kao funkciju članicu, ali ovaj put članicu klase “Tacka”, koja kao parametar prima objekat tipa “Krug”.
Ukoliko se odlučimo za običnu funkciju, njena izvedba može ostati posve ista kao u prethodnom
slučaju, a možemo je i prosto svesti na prethodni slučaj ovako:
inline Krug operator +(const Tacka &t, const Krug &k) { return k * t; }

S druge strane, izvedba kao funkcije članice klase “Tacka” mogla bi izgledati recimo ovako (ovu
funkciju dodajemo u klasu “Tacka”, a ne “Krug”):
Krug operator +(const Krug &k) const {
return Krug(k.DajBoju(), x + k.DajCentar().x, y + k.DajCentar().y,
k.DajPoluprecnik());
}

Obratimo pažnju da ovdje nemamo direktan pristup atributima klase “Krug”, tako da smo primorani
koristiti pristupne metode), ali zato imamo direktan pristup atributima klase “Tacka”. Moguća je i
jednostavnija izvedba, ukoliko parametar prenosimo po vrijednosti, tako da se kreira kopija:
Krug operator +(Krug k) const {
k.centar = Tacka(x + k.DajCentar().x, y + k.DajCentar().y); return k;
}

Konačno, i ovdje je moguće svesti izvedbu na ranije napisanu izvedbu koja podržava prvi operand
tipa “Krug” a drugi tipa “Tacka”, na sljedeći način:
Krug operator +(const Krug &k) const {
return k + *this;
}

Uglavnom, ko god je u potpunosti ispravno riješio podršku za sabiranje objekata tipa “Krug” sa
objektima tipa “Tacka” i obrnuto, dobija 0.7 poena. Ostaje još da se podrže operatori “*=” i “+=”.
Jednostavna analiza pokazuje da “*=” ima smisla samo kad je prvi operand tipa “Krug” a drugi realan
broj, dok “+=” ima smisla samo kad je prvi operand tipa “Krug” a drugi tipa “Tacka” (obrnuto nema
smisla, jer se kao rezultat sabiranja objekata tipa “Krug” i “Tacka” dobija objekat tipa “Krug”, što nije
moguće smjestiti u objekat tipa “Tacka”). Stoga je najprirodnije operatorske funkcije za ove operatore
izvesti kao funkcije članice klase “Tacka”. Najprirodnija izvedba funkcije za operator “*=” mogla bi
izgledati ovako:
Krug &operator *=(double x) { poluprecnik *= x; return *this; }

S druge strane, izvedba operatora “+=” zavisi od toga kako je izvedena klasa “Krug”. Ukoliko se centar
čuva kao atribut tipa “Tacka”, izvedba bi mogla izgledati ovako:
Krug &operator +=(const Tacka &t) {
centar = Tacka(centar.DajX() + t.DajX(), centar.DajY() + t.DajY());
return *this;
}

Međutim, ukoliko se koordinate centra čuvaju kao posebni atributi, možemo napraviti implementaciju
poput sljedeće:
Krug &operator +=(const Tacka &t) { xc += t.DajX(); yc += t.DajY(); return *this; }

Radi potpunosti, vrijedi ilustrirati i kako bi se operatori “*=” i “+=” mogli izvesti kao obične funkcije
(nečlanice), koje bi tada zbog potrebe pristupa privatnim dijelovima klase morale biti proglašene za
prijatelje klase “Krug”, tj. morali bismo u klasu dodati deklaracije
friend Krug &operator *=(Krug &k, double x);
friend Krug &operator +=(Krug &k, const Tacka &t);

Recimo, operator “*=” bi se preko prijateljske operatorske funkcije mogao izvesti ovako:
Krug &operator *=(Krug &k, double x) { k.poluprecnik *= x; return k; }

Naravno, i ovdje izvedba operatora “+=” zavisi od toga kako je izvedena klasa “Krug”. Ukoliko se
centar čuva kao atribut tipa “Tacka”, izvedba bi mogla izgledati recimo ovako:
Krug &operator +=(Krug &k, const Tacka &t) {
k.centar = Tacka(k.centar.DajX() + t.DajX(), k.centar.DajY() + t.DajY());
return k;
}

S druge strane, ukoliko se koordinate centra čuvaju kao posebni atributi, implementacija može izgledati
poput sljedeće:
Krug &operator +=(Krug &k, const Tacka &t) {
k.xc += t.DajX(); k.yc += t.DajY(); return k;
}

U svakom slučaju, ko god je ispravno podržao operatore “*=” i “+=”, dobija 0.6 poena. Naravno, pored
mnoštva prikazanih rješenja za podršku operatorima, moguće su još i brojne druge ispravne izvedbe,
uključujući razna “instant” rješenja opisana na predavanjima, poput izvedbe operatora “*=” i “+=”
koje se svode na izvedbu operatora “*” i “+” (ova rješenja se ne preporučuju), odnosno izvedbe
operatora “*” i “+”, koje se svode na izvedbu operatora “*=” i “+=”, što se također može izvesti na
mnogo načina (te izvedbe u suštini i nisu loše, ali nisu gotovo ništa jednostavnije od direktnih izvedbi
koje se ne oslanjaju na ove operatore).
Što se tiče još jedne verzije funkcije “RegistrirajLik” koju treba dodati, suštinka je stvar da treba
obaviti polimorfno kreiranje kopije objekta (pozivom funkcije “DajKopiju”), s obzirom na to da
referenca deklarirana na objekat tipa “Lik” zapravo može biti vezana za objekat bilo kojeg tipa
izvedenog iz tipa “Lik”. Ovdje ćemo pretpostaviti da je u prvom zadatku primijećeno da nam zapravo
treba funkcija “DajKopiju” i da je tamo i implementirana, a ko nije to tada primijetio, trebao je
primijetiti ovdje. Uglavnom, uz pretpostavku da je ta funkcija napisana onako kako smo to napisali u
prvom zadatku (da vraća pametni pokazivač), u klasu “KolekcijaLikova” treba dodati sljedeću
funkciju članicu:
void RegistrirajLik(const Lik &lik) {
likovi.push_back(lik.DajKopiju());
}

Ukoliko funkcija “DajKopiju” vraća obični pokazivač, tada bismo umjesto “lik.DajKopiju()” trebali
pisati “std::shared_ptr<Lik>(lik.DajKopiju())”. Uglavnom, funkciju “make_shared” ovdje ne
bismo mogli koristiti, jer ona mora tačno znati koji je tip objekta koji se kreira, a mi to ovdje ne znamo.
Ko je ispravno uočio potrebu za polimorfnim kopiranjem i ispravno implementirao ovu funkciju,
dobija 1 poen.

Konačno, potrebno je još izvesti podršku za direktan pristup tjemenima u klasi “Mnogougao” putem
operatora “[]”. Iz postavke je vidljivo da indeksacija treba da ide od jedinice, a kako ništa nije rečeno
o testiranju validnosti indeksa, nećemo ga ni implementirati (mada ga nikakav problem nije ni
implementirati). Što se tiče iščitavanja tjemena, ono je vrlo lagano za implementirati. Dovoljno je u
klasu “Mnogougao” dodati operatorsku funkciju članicu poput sljedeće (ne smijemo zaboraviti da je za
kreiranje objekata tipa “Tacka” neophodna i boja):
Tacka operator [](int redni_broj) const {
return Tacka(DajBoju(), x[redni_broj - 1], y[redni_broj - 1]);
}
Međutim, podrška izmjeni tjemena je mnogo problematičnija, i tu se predmetni nastavnik malo
preračunao, jer je previdio da se i koordinate tjemena čuvaju u različitim nizovima. U svakom
slučaju, operatorska funkcija za podršku tome ne bi trebala imati kvalifikaciju “const” i trebala bi
vratiti referencu na neki objekat tipa “Tacka”. Nažalost, nema nikakvog logičnog objekta tipa “Tacka”
na koji bi mogli vezati vraćenu referencu. Tijelo identično kao u prethodnoj funkciji čak se ne bi
moglo ni kompajlirati, jer se reference ne mogu vezati na privremene objekte koji nastaju pozivom
konstruktora kao funkcije. Sljedeće “rješenje”, koje se prirodno nameće,
Tacka &operator [](int redni_broj) {
Tacka t(DajBoju(), x[redni_broj - 1], y[redni_broj - 1]); // OVO NE RADI!!!
return t;
}

nije nažalost korektno, jer je u njemu “t” lokalna promjenljiva koja nestaje po završetku funkcije,
tako da vraćamo viseću referencu. Čak i da to nije slučaj, objekat “t” sadrži kopiju informacija o
koordinatama tjemena, i preko njega nikako ne bismo mogli promijeniti izvorne informacije. Ovaj
problem je nažalost nerješiv ukoliko tražimo “idelano” rješenje (koje radi u svim kontekstima), mada
postoje “polurješenja” koja rade u većini situacija. Uglavnom, ko je god uočio potrebu za postojanjem
dvije operatorske funkcije za operator “[]”, ispravno implementirao prvu, i uočio da postoje
problemi pri implementaciji druge, dobija 1 poen. Radi potpunosti, prikazaćemo kako bi moglo
izgledati i jedno “polurješenje”, mada se ne očekuje da bi ni jedan student koji se nije prije susreo sa
ovakvim rješenjima mogao samostalno doći do njega. Ideja je da se napravi mala “varka”, i da se iz
funkcije umjesto reference na objekat tipa tačka, vrati neki objekat koji će na izvjestan način “glumiti”
tu referencu (tzv. proxy objekat). Neka je tip tog objekta “Tacka_proxy”. Tada bi odgovarajuća
operatorska funkcija mogla izgledati ovako:
Tacka_proxy operator [](int redni_broj) {
return Tacka_proxy(DajBoju(), x[redni_broj - 1], y[redni_broj - 1]);
}

Ostaje samo da se napiše tip (klasa) “Tacka_proxy”. U objektima ovog tipa negdje u njihovim
atributima treba “zarobiti” reference (ili pokazivače) na elemente niza koji čuvaju koordinate tačke,
što treba uraditi u konstruktoru. Dalje, potrebno je dodati operatorsku funkciju za operator “=” koji
će primati kao parametar objekat tipa “Tacka”, tako da kad se god objektu tipa “Tacka_proxy”
vraćenom iz operatora “[]” pokuša dodijeliti neki objekat tipa “Tacka”, ta operatorska funkcija će
iskoristiti “zarobljene” informacije da preko njih pristupi onome što treba izmijeniti. Konačno, treba
implementirati operatorsku funkciju za konverziju tipa koja će u svim drugim kontekstima objekte
tipa “Tacka_proxy” automatski konvertirati u objekte tipa “Tacka”. U skladu s tim, implementacija
klase “Tacka_proxy” mogla bi izgledati recimo ovako:
class Tacka_proxy : public Lik {
double &x, &y;
public:
Tacka_proxy(Lik::Boja boja, double &x, double &y) : Lik(boja), x(x), y(y) {}
void operator =(const Tacka &t) { x = t.DajX(); y = t.DajY(); }
operator Tacka() const { return Tacka(DajBoju(), x, y); }
};

Ovakva rješenja sa proxy objektima često se koriste za realizaciju raznih “prljavih” trikova. Treba
napomenuti da realizacija proxy objekata nije uvijek tako jednostavna. Recimo, da je klasa “Tacka”
imala neke mutatorske metode, sve bi te metode također trebalo polnovo implementirati i u klasi
“Tacka_proxy”, da bi njihov poziv nad proxy objektom promijenio stvarni objekat kojeg taj proxy
objekat glumi, a ne kopiju objekta. Također, rješenja zasnovana na primjeni proxy objekata ne rade u
svim kontekstima, jer proxy objekat nikad ne može savršeno glumiti objekat koji bi trebao da
oponaša. Recimo, referenca na objekat tipa “Tacka” ne bi se mogla vezati na rezultat operatora “[]”,
jer ovaj operator vraća zapravo objekat tipa “Tacka_proxy”, a referenca na objekat tipa “Tacka” ne
može se vezati na tako nešto…

Zadatak 3 (4 poena)
Opišite sve razloge zbog kojih kontejnerska klasa “KolekcijaLikova” nije pogodna za direktno
smještanje njenog sadržaja u binarnu datoteku. Zatim dodajte u ovu klasu dvije funkcije članice
“SacuvajUDatoteku” i “ObnoviIzDatoteke” koje čuvaju cjelokupan sadržaj kontejnerske klase u
tekstualnoj datoteci, odnosno vrše obnavljanje njenog sadržaja iz tekstualne datoteke. Obje metode
traže ime datoteke kao parametar (tipa “string”). Datoteka treba da bude organizirana na sljedeći
način. U prvom redu datoteke nalazi se broj likova. Nakon toga, sljedeći podaci se redom ponavljaju za
svaki lik. Podaci za jedan lik su u jednom redu. Na početku reda se nalazi znak “T”, “M” ili “K”, ovisno da
li je u pitanju tačka, mnogougao ili krug (ovdje nećemo podržavati mogućnost da se naknadno mogu
dodavati i novi tipovi likova, jer bi to previše zakompliciralo stvari). Dalje su svi podaci koji slijede
međusobno razdvojeni razmacima. Za slučaj tačke, prvo se nalazi šifra boje (cijeli broj u opsegu od 0 do
6), te i koordinate tačke. Za slučaj mnogougla, imamo šifru boje, zatim broj tjemena, te koordinate
tjemena (onoliko parova brojeva koliko ima tjemena). Za slučaj kruga imamo šifru boje, zatim
koordinate centra, te na kraju poluprečnik. Ukoliko su već postojali podaci u kontejneru, prilikom
poziva funkcije “ObnoviIzDatoteke” oni trebaju prethodno biti obrisani. U slučaju problema pri radu sa
datotekama, treba baciti izuzetak tipa “logic_error”. Pri tome, prateći tekst treba biti “Kreiranje nije
uspjelo” ukoliko kreiranje datoteke nije uspjelo, “Datoteka ne postoji” ukoliko se pokuša obnova iz
nepostojeće datoteke, te “Problemi prilikom upisa u datoteku” odnosno “Problemi prilikom citanja iz
datoteke” u slučaju bilo kakvih drugih problema prilikom upisa u datoteku odnosno čitanja iz nje.

Rješenje:
Postoji nekoliko razloga zbog kojih kontejnerska klasa “KolekcijaLikova” nije pogodna za direktno
smještanje njenog sadržaja u binarnu datoteku. Na prvom mjestu, njen atribut “likovi” je vektor,
koji ne spada u skoro-POD tipove podataka, što samim time čini nepodesnim direktno smještanje
sadržaja ove klase u binarnu datoteku. Čak i da nije vektor u pitanju, njegovi elementi su pametni
pokazivači, koji ponovo nisu skoro-POD tipovi podataka, što je još jedna otežavajuća okolnost. Čak da
ni to nije slučaj, nego da imamo obične pokazivače, ti pokazivači pokazuju na objekte izvedene iz
klase “Lik” koja posjeduje virtualne metode, tako da svi ti objekti posjeduju skrivene pokazivače (na
tabelu virtualnih metoda). Sve ovo zajedno čini ovu klasu izrazito nepogodnom za direktno
smještanje njenog sadržaja u binarnu datoteku (to ne znači da nisu mogući razni indirektni postupci,
ali oni svakako zahtijevaju mnogo više znanja i truda nego što posjeduje prosječan student).
Uglavnom, ko ispravno detektira sve razloge zbog kojih direktno smještanje sadržaja ove klase u
binarnu datoteku nije pogodno, dobija 0.5 poena.

Razmotrimo sada kako bi mogla izgledati implementacija metode “SacuvajUDatoteku” koja snima
sadržaj primjeraka ove klase u tekstualnu datoteku. Najuniverzalniji način da se to uradi je da se ta
metoda izvede slično kao i metoda “IspisiKolekciju”, samo da se umjesto metode “Ispisi” poziva
neka metoda (nazovimo je “UpisiUDatoteku”) koja bi bila apstraktna u klasi “Lik” a konkretno
implementirana u svakoj naslijeđenoj klasi, koja odgovarajuće informacije upisuje u datoteku umjesto
da ih ispisuje na ekran (ta metoda bi primala kao parametar odgovarajući objekat toka). Ta izvedba
mogla bi izgledati ovako (umjesto rangovske for-petlje mogli smo koristiti i običnu for-petlju, kao u
metodi “IspisiKolekciju”):
void SacuvajUDatoteku(std::string ime) {
std::ofstream dat(ime);
if(!dat) throw std::logic_error("Neuspjesno kreiranje datoteke");
dat << likovi.size() << std::endl;
for(auto lik : likovi) lik->UpisiUDatoteku(dat);
if(!dat) throw std::logic_error("Problemi pri upisu u datoteku");
}

Naravno, potrebno je implementirati i odgovarajuće verzije metoda “UpisiUDatoteku”, što bi moglo


izgledati recimo ovako:
virtual void UpisiUDatoteku(std::ofstream &dat) const = 0; // U klasi Lik
void UpisiUDatoteku(std::ofstream &dat) const override { // U klasi Tacka
dat << "T" << DajBoju() << " " << x << " " << y << std::endl;
}
void UpisiUDatoteku(std::ofstream &dat) const override { // U klasi Mnogougao
dat << "M" << DajBoju() << " " << br_tjemema << " ";
for(int i = 0; i < br_tjemena; i++) dat << x[i] << " " << y[i] << " ";
dat << std::endl;
}
void UpisiUDatoteku(std::ofstream &dat) const override { // U klasi Krug
dat << "K" << DajBoju() << " " << centar.DajX() << " " <<
centar.DajY() << " " << poluprecnik << std::endl;
}
Ovo rješenje je vrlo praktično ukoliko se pojave nove klase izvedene iz klase “Lik” (samo je potrebno
implementirati odgovarajuću metodu “UpisiUDatoteku” u novoj klasi). Manje fleksibilno i nezgrapnije
rješenje je da se ispituje tip objekta na koji odgovarajući pokazivač pokazuje i da se u zavisnosti od toga
vrše odgovarajuće radnje. To ispitivanje možemo vršiti pomoću “typeid” operatora, ali tu se javlja
dodatni problem. Čak i nakon što utvrdimo stvarni tip objekta, moramo vršiti pretvorbu pokazivača
naniže (downcast), jer preko pokazivača deklariranog na objekat tipa “Lik” možemo pristupiti samo
onome što je deklarirano u klasi “Lik”. Međutim, umjesto da vršimo posebno dvije stvari (testiranje i
pretvorbu), pametnije je obje stvari izvesti “jednim udarcem” koristeći dinamičku pretvorbu putem
operatora ”dynamic_cast” (tačnije ”dynamic_pointer_cast” jer su u pitanju pametni pokazivači).
Time ujedno vršimo pretvorbu ako je ona moguća, ili dobijamo nul-pokazivač kao rezultat ako
pretvorba nije moguća, što se dešava ukoliko pokazivač ne pokazuje na objekat traženog tipa). U skladu
s tim, dolazimo do rješenja poput sljedećeg:
void UpisiUDatoteku(std::string ime) {
std::ofstream dat(ime);
if(!dat) throw std::logic_error("Neuspjesno kreiranje datoteke");
dat << likovi.size() << std::endl;
for(auto lik : likovi)
if(auto p = dynamic_pointer_cast<Tacka>(lik))
dat << "T" << p->DajBoju() << " " << p->x << " " << p->y << std::endl;
else if(auto p = dynamic_pointer_cast<Mnogougao>(lik)) {
dat << "M" << p->DajBoju() << " " << p->br_tjemema << " ";
for(int i = 0; i < br_tjemena; i++) dat << p->x[i] << " " << p->y[i] << " ";
dat << std::endl;
}
else if(auto p = dynamic_pointer_cast<Krug>(lik))
dat << "K" << p->DajBoju() << " " << p->centar.DajX() << " " <<
p->centar.DajY() << " " << p->poluprecnik << std::endl;
if(!dat) throw std::logic_error("Problemi pri upisu u datoteku");
}

Primijetna je neelegancija ovog rješenja (poznata kao if-ologija), koja posebno dolazi do izražaja
ukoliko treba dodati i nove tipove likova. Uglavnom, ko god je ispravno riješio problem upisa sadržaja
objekata tipa “KolekcijaLikova” (a nije niko), dobija 1.8 poena.

Postupak obnove sadržaja iz datoteke je relativno jednostavan, ali pomalo glomazan i dosadan. Ovdje
moramo koristiti if-ologiju, jer postupak obnove ovisi od znaka koji pročitamo iz datoteke. Sam tok
funkcije je lagan za pratiti, pa ga nećemo detaljno opisivati. Uglavnom, ideja je da se prvo pročita
informacija o broju likova, nakon čega znamo tačno koliko podataka trebamo čitati. U svakom redu,
prvo pročitamo znak koji kodira tip lika, nakon čega postupamo drugačije za svaki tip lika posebno.
Cilj je na osnovu pročitanih podataka kreirati ispravno odgovarajući lik (što zahtijeva prikladnu
dinamičku alokaciju) i registrirati ga pozivom metode “RegistrirajLik”:
void ObnoviIzDatoteke(std::string ime) {
likovi.resize(0);
std::ifstream dat(ime);
if(!dat) throw std::logic_error("Datoteka se ne moze otvoriti");
int br_likova;
dat >> br_likova;
if(!dat) throw std::logic_error("Problemi pri citanju datoteke");
for(int i = 1; i <= br_likova; i++) {
char tip;
dat >> tip;
Lik::Boje boja;
double x, y, poluprecnik;
if(tip == 'T') {
dat >> boja >> x >> y;
RegistrirajLik(new Tacka(boja, x, y));
}
else if(tip == 'M') {
int br_tjemena;
dat >> boja >> br_tjemena;
Mnogougao *m(new Mnogougao(boja, br_tjemena));
for(int i = 1; i <= br_tjemena; i++) {
dat >> x >> y;
m->PostaviTjeme(i, x, y);
}
RegistrirajLik(m);
}
else if(tip == 'K') {
dat >> boja >> x >> y;
RegistrirajLik(new Krug(boja, x, y, poluprecnik));
}
else throw std::logic_error("Nepoznat tip lika");
if(!dat) std::logic_error("Problemi pri citanju datoteke");
}
}

Treba primijetiti da prije nego što uđemo u for-petlju provjeravamo da li je uspješno obavljeno čitanje
informacije o broju likova, u suprotnom besmisleno je i ulaziti u petlju. Ko je uspješno izveo ovu
funkciju, dobija 1.7 poena.

You might also like