Prva laboratorijska vježba iz Oblikovnih obrazaca u programiranju:
dinamički polimorfizam i načela oblikovanja

1) Ova vježba razmatra ostvarivanje dinamičkog polimorfizma u programskom jeziku C. Potrebno je napisati kod niže razine koji bi omogućio ispravno izvršavanje priložene ispitne funkcije.

void testAnimals(void){
  struct Animal* p1=createDog("Hamlet");
  struct Animal* p2=createCat("Ofelija");
  struct Animal* p3=createDog("Polonije");

  animalPrintGreeting(p1);
  animalPrintGreeting(p2);
  animalPrintGreeting(p3);

  animalPrintMenu(p1);
  animalPrintMenu(p2);
  animalPrintMenu(p3);

  free(p1); free(p2); free(p3);
}
Prikazana ispitna funkcija treba generirati sljedeći ispis.
  Hamlet pozdravlja: vau!
  Ofelija pozdravlja: mijau!
  Polonije pozdravlja: vau!
  Hamlet voli kuhanu govedinu
  Ofelija voli konzerviranu tunjevinu
  Polonije voli kuhanu govedinu
Pretpostavimo da su funkcije koje definiraju ponašanje konkretnih tipova zadane kako slijedi.
char const* dogGreet(void){
  return "vau!";
}
char const* dogMenu(void){
  return "kuhanu govedinu";
}
char const* catGreet(void){
  return "mijau!";
}
char const* catMenu(void){
  return "konzerviranu tunjevinu";
}
Potrebno je oblikovati sljedeće elemente rješenja. Obratite pažnju na to da deklaracija PTRFUN pfun; u C-u (ali ne i C++-u!) definira pokazivač na funkciju s nespecificiranim argumentima. To znači da pfun može pokazivati na bilo koju funkciju koja vraća char const* (detalji). Naravno, pri korištenju pokazivača pfun moramo paziti da broj i tipovi argumenata navedeni u pozivu odgovaraju argumentima funkcije na koju pokazivač pokazuje (u suprotnom ponašanje programa nije definirano).

Nakon rješavanja zadatka, uspostavite vezu s terminologijom iz objektno orijentiranih jezika. Koji elementi vašeg rješenja bi korespondirali s podatkovnim članovima objekta, metodama, virtualnim metodama, konstruktorima, te virtualnim tablicama?

2) Ova vježba razmatra memorijsku cijenu dinamičkog polimorfizma. Vježbu ćemo provesti u okviru jezika C++, ali analogni zaključci bi vrijedili i u ostalim jezicima. Neka su zadani tipovi CoolClass i PlainOldClass kako slijedi.

  class CoolClass{
  public:
    virtual void set(int x){x_=x;};
    virtual int get(){return x_;};
  private:
    int x_;
  };
  class PlainOldClass{
  public:
    void set(int x){x_=x;};
    int get(){return x_;};
  private:
    int x_;
  };
Ispitaj memorijske zahtjeve objekata dvaju tipova (pomoć: ispiši sizeof(PlainOldClass) i sizeof(CoolClass)). Objasni dobivenu razliku.

3) Ova vježba razmatra vremensku cijenu dinamičkog polimorfizma. Vježbu ćemo provesti u okviru jezika C++, ali analogni zaključci bi vrijedili i u ostalim jezicima. Neka je zadana nova verzija razreda CoolClass te novi ispitni glavni program, dok izvedbu razreda PlainOldClass preuzimamo iz prethodnog zadatka.

  class Base{
  public:
    //if in doubt, google "pure virtual"
    virtual void set(int x)=0;
    virtual int get()=0;
  };
  class CoolClass: public Base{
  public:
    virtual void set(int x){x_=x;};
    virtual int get(){return x_;};
  private:
    int x_;
  };
  int main(){
    PlainOldClass* poc=new PlainOldClass;
    Base* pb=new CoolClass;
    poc->set(42);
    pb->set(42);
  }  
Promotri kako je prevoditelj izveo pozive Base::set i PlainOldClass::set (pomoć: prevedi datoteku s g++ -O0 -S -masm=intel file.cpp, te potraži konstantu 42 u datoteci file.s; gcc za Appleova računala ne podržava opciju -masm=intel pa je treba izostaviti i snaći se u AT&T-jevoj sintaksi, ili instalirati neku od novijih verzija clanga te prevođenje provesti s clang++ -O0 -S -mllvm --x86-asm-syntax=intel file.cpp). Objasni razliku između izvedbi tih dvaju poziva. Koji od ta dva poziva je moguće obaviti uz manje instrukcija? Za koju od te dvije izvedbe bi optimirajući prevoditelj mogao generirati kôd bez instrukcije CALL odnosno umetanjem kôda (inlining)? Ekstra zadatak za strpljive: pokušajte pronaći asemblerski kôd za definiciju i inicijalizaciju tablice virtualnih funkcija razreda CoolClass. Veliku pomoć u dešifriranju dekoriranih imena identifikatora može vam pružiti alat c++filt (uputstvo, mrežno sučelje).

Upute za analiziranje strojnog koda. Osnova za strojni jezik danas sveprisutnih Intelovih računala nastala je u davnim 70-im godinama prošlog stoljeća. Kako bi ostvarili kompatibilnost sa starim programima, nove generacije računala podržavale su sve prethodnike te istovremeno uvodile nove instrukcijske podskupove. Moderna Intelova računala imaju preko 1000 instrukcija te istovremeno podržavaju 78 instrukcija procesora 8080. Stoga najčešće nema smisla učiti cijeli instrukcijski skup, nego je najbolje početi od jednostavnih instrukcija koje postoje i na arhitekturama koje ste upoznali u uvodnim kolegijima. Registri arhitekture x86 označavaju se s eax, ebx, ecx, edx, esi, edi, ebp i esp (kazalo stoga), dok se 64-bitne verzije tih registara označavaju na način da slovo e zamijenimo s r (npr. rax, rbx itd.). Većina instrukcija arhitekture x86 imaju jedan izvorišni i jedan odredišni operand. Izvorište može biti konstanta, memorijska adresa ili registar, dok odredište može biti memorijska lokacija ili registar (može biti najviše jedan memorijski operand). Obratite pažnju da se u originalnoj Intelovoj sintaksi prvo navodi odredišni operand, dok je u AT&T-jevoj sintaksi obratno. Npr. instrukcija mov DWORD PTR [esp+4], 42 prebacuje konstantu 42 na adresu esp+4, dok instrukcija add esp, 44 uvećava esp za 44. Instrukcija call x poziva potprogram, pri čemu odredište x može biti zadano konstantom (statički poziv potprograma) ili registrom (tako se najčešće izvode dinamički pozivi). Povratak iz potprograma provodimo instrukcijom ret. Onima koji žele saznati više preporučamo laboratorijske vježbe Arhitekture računala 2 te sljedeće upute: 1, 2.

4) Ova vježba ukazuje na različito ponašanje polimorfnih poziva tijekom i nakon završene konstrukcije objekta. Pokušajte objasniti ispis programa analizirajući prevedeni strojni kod.

#include <stdio.h>

class Base{
public:
  Base() {
    metoda();
  }

  virtual void virtualnaMetoda() {
    printf("ja sam bazna implementacija!\n");
  }

  void metoda() {
    printf("Metoda kaze: ");
    virtualnaMetoda();
  }
};

class Derived: public Base{
public:
  Derived(): Base() {
    metoda();
  }
  virtual void virtualnaMetoda() {
    printf("ja sam izvedena implementacija!\n");
  }
};

int main(){
  Derived* pd=new Derived();
  pd->metoda();
}

5) Prevedi i isprobaj program zadan na stranici predavanja "Tehnike: C++". Promijeni redak:

Derived d;

u

Derived2 d;

Dodaj još i:

#include "derived2.hpp"

Pokreni program.

Implementiraj novu izvedenu klasu Derived3 koja nasljeđuje Base. Sučelje stavi u datoteku derived3.hpp, implementacijsku datoteku ne moraš imati (sve metode mogu biti "umetnute", inline). Sada u glavnom programu uključi derived3.hpp, te umjesto Derived2 navedi Derived3. Pokreni program.

Stara implementacija klijenta radi s novom izvedbom osnovne klase. Klijenta nismo morali ni pipnuti (čak ni ponovo prevesti!). Koje načelo kaže da *stari kod* treba moći koristiti *novi kod*?

Za strpljive: napišite odgovarajuće programe u Pythonu i Javi.

6) Prevedi i isprobaj priloženi dopunjeni program s predavanja (str.~``Logička načela: NBP i proceduralni stil?'').

  #include <iostream>
  #include <assert.h>

  struct Point{
    int x; int y;
  };
  struct Shape{
    enum EType {circle, square};
    EType type_;
  };
  struct Circle{
     Shape::EType type_;
     double radius_;
     Point center_;
  };
  struct Square{
     Shape::EType type_;
     double radius_;
     Point center_;
  };
  void drawSquare(struct Square*){
    std::cerr <<"in drawSquare\n";
  }
  void drawCircle(struct Circle*){
    std::cerr <<"in drawCircle\n";
  }
  void drawShapes(Shape** shapes, int n){
    for (int i=0; i<n; ++i){
      struct Shape* s = shapes[i];
      switch (s->type_){
      case Shape::square:
        drawSquare((struct Square*)s);
        break;
      case Shape::circle:
        drawCircle((struct Circle*)s);
        break;
      default:
        assert(0); 
        exit(0);
      }
    }
  }
  int main(){
    Shape* shapes[4];
    shapes[0]=(Shape*)new Circle;
    shapes[0]->type_=Shape::circle;
    shapes[1]=(Shape*)new Square;
    shapes[1]->type_=Shape::square;
    shapes[2]=(Shape*)new Square;
    shapes[2]->type_=Shape::square;
    shapes[3]=(Shape*)new Circle;
    shapes[3]->type_=Shape::circle;

    drawShapes(shapes, 4);
  }

Dodaj metodu moveShapes koja pomiče oblike zadane prvim argumentom za translacijski pomak određen ostalim argumentima. Ispitaj dodanu funkcionalnost.

Dodaj razred Rhomb. Dodaj jedan objekt tipa Rhomb u listu objekata u main(). Sjeti se, sad moramo promijeniti i drawShapes().

Ovo je domino-efekt (krutost), kojeg ćemo kasnije pokušati zauzdati. Za probu, zaboravi adekvatno promijeniti moveShapes(). Isprobaj ponovo. Sad bi moveShapes trebao "puknuti". To je krhkost uzrokovana redundancijom. Ni to ne želimo imati u programu.

Konačno, implementiraj rješenje s predavanja, i komentiraj njegova svojstva.

7) Prevedi i pokreni program na stranici ``Logička načela: NIO, injekcija ovisnosti'' (u izvorni kod je potrebno dodati deklaracije i implementacije razreda AbstractDatabase i MockDatabase). Objasni načela inverzije i injekcije ovisnosti.