Pitanje

Prerađivanjem metode iz bazne klase u proširenoj klasi želimo nasljediti sučelje metode i promijeniti implementaciju.

class Window{
public:
    void resize(int x, int y) {
    std::cout << "Window::resize"  << std::endl;
    // ....
}
// .....
};

class SimpleDialog : public Window{
public:
    void resize(int x, int y) {
    Window::resize(x,y);   // Neka bazna klasa napravi svoj dio posla ...
    std::cout << "SimpleDialog::resize"  << std::endl;
    // ....
}
// .....
};

Funkcija update() je klijent klase Window. Kako uzima referencu na Window može raditi sa svakom klasom koja je izvedena iz klase Window.

void update(Window& obj)
{
    // ...
    int x=80, y=170;
    //
    obj.resize(x,y);
    // ...
}

Koja će metoda resize() biti pozvana kada update() dobije SimpleDialog objekt?

SimpleDialog dialog;
update(dialog);

Odgovor

Bit će pozvana metoda resize() iz klase Window!

Da bi se funkcija update() zaista ponašala polimorfno, ona bi morala pozvati verziju metode resize() implementiranu u klasi SimpleDialog. Bez toga ne može adekvatno promijeniti dimenziju SimpleDialog prozora.

Da bismo postigli polimorfno ponašanje moramo izmijeniti baznu klasu Window. U njoj funkciju resize() trebamo proglasiti virtualnom:

class Window{
public:
    virtual void resize(int x, int y) {
        std::cout << "Window::resize"  << std::endl;
        // ....
    }
    // .....
};

Kada se virtualna metoda poziva kroz pokazivač ili referencu na baznu klasu poziva se verzija metode iz klase koja je određena dinamičkim tipom pokazivača, odnosno reference.

Sada se u kodu

SimpleDialog dialog;
update(dialog);    // zove SimpleDialog::resize()

na referenci dialog zove metoda update() iz klase SimpleDialog.

Napomena
Polimorfizam je termin koji označava primjenu jednog te istog koda na različite vrste objekata. Objekti moraju pripadati istoj hijerarhiji klasa, a postiže se kroz mehanizam virtualnih funkcija. Polimorfno se mogu ponašati samo klase koje definiraju barem jednu virtualnu funkciju te se stoga takve klase nazivaju polimornim klasama.

Jednostavan primjer

Bazna klasa

class Base{
public:
    virtual void f(){ cout << "Base::f()\n"; }
    void g(){ cout << "Base::g()\n"; }
    virtual void h(){ cout << "Base::h()\n"; }
};

Izvedena klasa

class Derived : public Base{
public:
    virtual void f(){ cout << "Derived::f()\n"; }
    void g(){ cout << "Derived::g()\n"; }
    void z(){ cout << "Derived::z()\n"; }
};

Klijent

void client(Base * pb){
    pb->f();
    pb->g();
    pb->h();
//  pb->z(); - daje grešku pri kompilaciju
}

Poziv klijenta

int main() {
    Derived d;
    client(&d);

    return 0;
}

Rezultat

Derived::f()
Base::g()
Base::h()
  • Funkcija f() je virtualna i prerađena je u izvedenoj bazi te dobivamo prerađenu verziju metode.

  • Funkcija g() nije virtualna i premda je prerađena u izvedenoj klasi dobivamo verziju metode iz bazne klase.

  • Funkcija h() je virtualna ali nije prerađena je u izvedenoj klasi te dobivamo metodu iz bazne klase.

  • Funkcija z() ne postoji u baznoj klasi i stoga ju klijent ne može pozvati.

Virtualne metode

  • Kada je funkcija članica bazne klase označena virtual onda se mijenja način na koji se funkcija poziva ako poziv ide kroz pokazivač ili referencu. Ako se statički i dinamički tip pokazivača ili reference razlikuju, onda će biti pozvana verzija virtualne funkcije iz klase koja odgovara dinamičkom tipu.

  • Dinamički tip neke reference ili pokazivača nije poznat za vrijeme kompilacije i stoga se odluka koja će verzija virtualne funkcije biti pozvana ne može donijeti tijekom kompilacije: ona se donosi za vrijeme izvršavanja programa i taj se proces naziva dinamičko vezanje (eng. dynamic binding).

  • Metodu činimo virtualnom u baznoj klasi kada ju želimo redefinirati u proširenim klasama i kada želimo da implementacija iz proširene klase bude korištena u metodama koje su klijenti bazne klase (kroz pokazivače i reference). Oznaka virtual znači da proširena klasa nasljeđuje samo sučelje (deklaraciju) funkcije, ali može supstituirati vlastitu implementaciju.

Napomena
Objekti nemaju dinamički tip. Kada funkciju zovemo na objektu (na primjer, A a; a.f()) tada nema nikakve razlike između virtualne i nevirtualne funkcije. Uvijek se zove funkcija A::f().

Implementacija polimorfizma

  • Za svaku klasu koja sadrži barem jednu virtualnu funkciju prevodilac kreira virtualnu tabelu klase koja sadrži pokazivače na virtualne funkcije klase. Nevirtualne funkcije klase ne ulaze u tu tabelu. Virtualna tabela je jedna za sve instance (objekte) dane klase.

  • U svaku instancu klase prevodilac stavlja pokazivač na virtualnu tabelu klase vtp (virtual table pointer). Radi se o jednom pokazivaču, neovisno o tome koliko ima virtualnih funkcija u klasi. Prisustvo virtualne funkcije u klasi povećava stoga svaku instancu klase za jedan pokazivač. K tome se još stavlja "skriveni kod" u konstruktor koji inicijalizira vtp pokazivač.

  • Kada treba pozvati virtualnu funkciju prvo se dohvati pokazivač na virtualnu tabelu (vtp) koji sadrži svaki objekt. On je određen tipom objekta i stoga se selekcija virtualne metode vrši prema dinamičkom tipu. Odgovarajuću metodu prevodilac selektira računanjem offset-a u virtualnoj tabeli (koji se zna za vrijeme kompilacije) i zatim se funkcija poziva kroz njen pokazivač.

vptr.png
class Base{
    public:
        virtual int f(int);
        virtual void g(int);
        int h();
    private:
        int a;
};

class Derived : public Base {
    public:
        virtual int f(int);
        virtual void w(int);
    private:
        int b;
};
base-derived.png

Kovarijantni povratni tip

  • Reimplementacija virtualne funkcije u proširenoj klasi mora imati isti broj i tip parametara te isti povratni tip kao i funkcija u baznoj klasi. To pravilo ima jednu iznimku. Ako virtualna funkcija u baznoj klasi vraća pokazivač ili referencu na baznu klasu, onda njena prerađena verzija u proširenoj klasi može vratiti pokazivač ili referencu na proširenu klasu klasu.

Bazna klasa - metoda window() vraća referencu na baznu klasu

class Window{
    public:
        // .....
        virtual Window& window(){
         // Vraća referencu na Window
         // ...
            return *this;
        }
};

Izvedena klasa - metoda window() vraća referencu na izvedenu klasu

class SimpleDialog : public Window{
    public:
        // .....
        virtual SimpleDialog& window(){
         // O.K. Vraća referencu na SimpleDialog a ne Window
         // ...
            return *this;
        }
};

Funkciju je dovoljno deklarirati virtualnom samo u baznoj klasi

Prerađene verzije funkcije su automatski virtualne.

class Window{
  public:
      virtual void resize(int x, int y) {
        // Ovdje je deklaracija virtual bitna
        // ....
      }
      // .....
};

class SimpleDialog : public Window{
  public:
      void resize(int x, int y) {
          // Ovdje je deklaracija virtual nije potrebna
          // i stavlja se samo kao informacija
          // ....
      }
      // .....
};

Operator dosega :: zaobilazi virtualni mehanizam

  • Virtualni mehanizam se može zaobići pomoću operatora dosega. Tako na primjer u metodi update možemo pomoću obj.Window::resize(x,y); pozvati metodu resize iz bazne klase neovisno o dinamičkom tipu reference obj. Analogno vrijedi za poziv kroz pokazivače.

void update(Window& obj)
{
    // ...
    int x=80, y=170;
    //
    obj.Window::resize(x,y); // Poništava virtualni mehanizam
                             // i uvijek zove metodu iz bazne klase ...
}

Napomene

  • Konstruktori nikad nisu virtualni: to ne bi imalo smisla jer dok se konstruktor izvršava objekt još nije kompletan i njegov dinamički tip nije poznat.

  • Operator pridruživanja se može učiniti virtualnim, ali to nije korisno.

  • Ako virtualna funkcija ima defaultne parametre i pozvana je kroz pokazivač ili referencu koriste se defaultni parametri prema statičkom tipu pokazivača ili reference. Stoga prerađena funkcija mora zadržati defaultne vrijednosti bazne funkcije, inače ponašanje može biti neočekivano.

Virtualni destruktor

Čak ako i nemamo potrebe za destruktorom u baznoj klasi trebamo definirati virtualni destruktor ako želimo da klasa služi kao baza za definiciju proširenih klasa.

Bazna klasa bez virtualnog destruktora

struct A {
    A(double x=0.0, double y=0.0, double z=0.0)
    { coo[0]=x; coo[1]=y; coo[2]=z; }

    inline double mean(){
        // ....
    }
    double coo[3];
    ~A(){ std::cout << "~A()"<<std::endl; }  // nevirtualni konstruktor
};

Izvedena klasa

struct B :  A // javno nasljeđivanje (default)
{
    B(double x=0.0, double y=0.0, double z=0.0, const std::string& _name="B") :
                         A(x,y,z), name(_name) {}
    std::string get_name() { return name; }
    ~B(){ std::cout << "~B()"<<std::endl; }
    std::string name;
};

Gubitak memorije

int main()
{
    A *pa = new B(3,6,3);
    // ...
    std::cout << pa->mean() << std::endl;
    delete pa;  // Gubitak memorije: pozvan je samo destruktor klase A
    return 0;
}
  • Ovdje smo (u naredbi delete pa;) kroz pokazivač čiji je statički tip jednak baznom tipu pozvali nevirtualni destruktor bazne klase. Posljedica toga je da se izvršila destrukcija samo baznog dijela objekta.

delete_base.png
  • Kada destruktor definiramo virtualnim onda se zove destruktor prema dinamičkom tipu objekta — dakle, destruktor izvedene klase. Prevodilac nakon destrukcije izvedenog dijela klase automatski poziva i destruktor baznog dijela i objekt će biti potpuno uništen.

delete_derived.png

Čista virtualna funkcija i apstraktna klasa

U baznoj klasi ponekad nemamo pravu implementaciju za neku od virtualnih funkcija. U tom slučaju funkciju možemo deklarirati kao čistu virtualnu funkciju tako što ćemo ju izjednačiti s nulom. Na primjer,

virtual void resize(int x, int y)  = 0;

Tom deklaracijom kažemo da funkcija nema implementaciju; nju će dati proširene klase. Klasa koja ima barem jednu čistu virtualnu metodu je apstraktna klasa. Prevodilac neće dozvoliti instanciranje objekata apstraktne klase. Njena uloga je da služi kao bazna klasa za hijerarhiju klasa koja će biti izvedena iz nje i stoga možemo koristiti samo pokazivače i reference na apstraktnu klasu.

Sljedeći primjer ilustrira apstraktnu baznu klasu.

// apstraktna bazna klasa
class Base{
    public:
        virtual void f() const = 0; // čista virtualna funkcija
        virtual void g() const = 0; // čista virtualna funkcija
};
// konkretna klasa daje implementaciju čistim virtualnim
// funkcijama
class Derived : public Base{
    public:
        void f() const { /*  implementacija */ }
        void g() const { /*  implementacija */ }
};

// klijent bazne klase. Može uzeti samo referencu ili pokazivač na
// Base.
void process(Base const & obj)
{
    obj.f();
    obj.g();
}

int main(){
    Base obj;     // greška - apstraktna klasa
    Derived obj;  // o.k.
    process(obj); // o.k.
}

override, C++2011

Kod prerađivanja virtualne bazne funkcije lako je pogriješiti kao u sljedećem primjeru:

struct Base{
    virtual void f() ;
};
struct Derived : Base{
    void f(int);  // prerađujemo f() ali, greška!
                  // Base::f() ne uzima parametar int.
};

Mislili smo da prerađujemo funkciju void f() iz Base, ali smo ju samo sakrili novom funkcijom u Derived. Za prevodilac to nije greška, premda najčešće jeste.

U standardu C++11 možemo izraziti namjeru prerađivanja virtualne funkcije pomoću override. U tom slučaju gornja situacija završava greškom pri kompilaciji.

struct Base{
  virtual void f(int) const;
  virtual void g();
  void h();
};

struct Derived : Base{
  void f(int) const override; // ok: f odgovara f iz bazne klase
  void g(int) override;       // greška: Base nema g(int)
  void h() override;          // greška: h nije virtualna
  void k() override;          // greška: B nema funkciju k()

};

Sada prevodilac javlja grešku u tri slučaja u kojima funkcija ne prerađuje virtualnu funkciju iz bazne klase.

  • override dolazi na kraj deklaracije funkcije.

final, C++11

Ukoliko želimo zabraniti (daljnje) prerađivanje virtualne funkcije možemo ju proglasiti finalnom pomoću final. Ključna riječ final dolazi na kraju deklaracije. Primjer:

class Base{
    public:
        virtual void f() const;
        void g();
};

class Derived : public Base{
    public:
        void f() const final; // zabranjujemo daljnje prerađivanje
        void g() final;       // greška final se može primijeniti samo
                              // na virtualnu funkciju
};

class D2 : public Derived{
    public:
        void f() const;  // greška: prerađujemo finalnu funkciju
};
  • final se može primijeniti samo na virtualnu funkciju i ne ponavlja se u definiciji fukcije izvan klase.

Ako želimo spriječiti proširivanje neke klase možemo ju deklarirati kao final. Ključna riječ final dolazi iza imena klase, a ispred eventualne derivacijske liste (dvotočke).

class  A {
  // ...
};

class B final : public A{
  // ...
};

class C : public B { // greška, ne možemo proširivati B
  // ...
};

Polimorfizam i spremnici

Stablo nasljeđivanja:

class A {
   public:
      virtual void name() const {  std::cout << "class A" << std::endl; }
};

class B : public A
{
   public:
      virtual void name() const {  std::cout << "class B" << std::endl; }
};

Koja će metoda biti pozvana?

int main()
{
    std::vector<A>  vec_A;
    A a;
    B b;
    vec_A.push_back(a);
    vec_A.push_back(b);

    vec_A[0].name();
    vec_A[1].name();

    return 0;
}
  • Kako se objekti predaju spremnicima kopiranjem doći će do izrezivanja baznog podobjekta u naredbi vec_A.push_back(b); Stoga će uvijek biti pozvana metoda name() iz bazne klase.

  • Kako se objekti ne ponašaju polimorfno u spremnicima trebamo raditi s pokazivačima.

Polimorfno ponašanje dobivamo ako spremamo pokazivače

int main()
{
    std::vector<A*> vec_pA;

    A a;
    B b;

    vec_pA.push_back(&a);
    vec_pA.push_back(&b);

    vec_pA[0]->name();
    vec_pA[1]->name();

    return 0;
}
  • Sada se ispravno poziva metoda prema dinamičkom tipu pokazivača.

  • Ponovo se javlja problem dealokacije memorije (ako ju dinamički alociramo)! Izlaz je u korištenju pametnih pokazivača, na primjer unique_ptr<A>.

U svrhu primjera generalizirajmo našu klasu SmartBFC tako da može uzeti pokazivač na proizvoljan tip.

Pametni pokazivači osiguravaju polimorfno ponašanje i automatsku dealokaciju

U spremnik treba umjesto golih pokazivača spremati pametne pokazivače.

Sljedeći kod sada funkcionira korektno i virtualna funkcija će biti pozvana prema svojem dinamičkom tipu:

#include <iostream>
#include <memory>
#include <vector>

class A {
   public:
      virtual void name() const {  std::cout << "class A" << std::endl; }
};

class B : public A
{
   public:
      virtual void name() const {  std::cout << "class B" << std::endl; }
};

int main()
{
    std::vector<std::unique_ptr<A>> vec;
    vec.push_back(std::unique_ptr<A>(new A()));
    vec.push_back(std::unique_ptr<A>(new B()));

    vec[0]->name();  // ispisuje class A
    vec[1]->name();  // ispisuje class B

    return 0;
}

Polimorfizam i kreiranje objekta nepoznatog tipa

Problem

Pametni pokazivač funkcionira samo s objektima kreiranim na hrpi (dinamički alociranim). Sljedeći kod završava sa greškom:

B bb;
SmartPointer<B> spbb(&bb);

Zašto?

Rješenje

Da bismo riješili taj problem SmartPointer treba dobiti konstruktor koji će umjesto pokazivača na dinamički alocirani objekt dobiti kao argument referencu na objekt:

SmartPointer(const T & t);

SmartPointer će sada lokalno dinamički alocirati kopiju objekta i držati pokazivač na nju. Pri izlasku iz dosega dinamički alocirana kopija objekta će biti dealocirana. Ostaje nam još osigurati polimorfno ponašanje.

  • To znači da pametni pokazivač koji drži pokazivač statičkog tipa T treba korektno dinamički alocirati objekt svakog tipa koji proširuje T. Drugim riječima, moramo korektno dinamički locirati objekt nepoznatog tipa.

Kloniranje

Tu smo se susreli s problemom konstrukcije objekta čiji nam tip nije poznat ali za koji znamo da proširuje danu baznu klasu.

On se rješava tako što se u svakoj klasi u lancu nasljeđivanja definira virtualna funkcija koja dinamički alocira objekt tipa klase i vraća pokazivač na njega. Tu ćemo funkciju zvati clone().

class A {
  public:
     // ...
     virtual void name() const {  std::cout << "class A" << std::endl; }
     virtual A* clone() const { return new A(*this); } // Kloniraj objekt
};

class B : public A
{
  public:
     // ...
     virtual void name() const {  std::cout << "class B" << std::endl; }
     virtual B* clone() const { return  new B(*this); }  // Kloniraj objekt
};

Nova klasa SmartPointer je sada dana ovdje:

Novi konstruktor SmartPointer klase

U našoj implementaciji pametnog pokativača na bazi brojanja referenci (SmartPointer) dodajemo novi konstruktor koji uzima objekt i klonira ga:

template <class T>
class SmartPointer{
  public:
    SmartPointer(T *p) : ptr(p), cnt(new int(1)) {}
      // Novi konstruktor
    SmartPointer(const T &t) : ptr(t.clone()), cnt(new int(1)) {}
     // ...
  private:
    T     * ptr;
    int   * cnt;
};

Problem je riješen uz dva ograničenja:

  • Klase koje želimo koristiti polimorfno moraju se znati klonirati;

  • Korisnik mora znati da SmartPointer klasi ne smije dati adresu objekta koji nije dinamički alociran. Umjesto adrese u toj situaciji treba SmartPointer klasi predati objekt.

Sljedeći kod je sada korektan:

A* pa = new A();
SmartPointer<A> spa(pa);   // Dinamički alociran objekt

B* pb = new B();
SmartPointer<A> spb(pb);   // Dinamički alociran objekt

B bb;                      // Statički objekt

std::vector< SmartPointer<A> > vect_A;

vect_A.push_back(spa);
vect_A.push_back(spb);
vect_A.push_back(SmartPointer<A>(bb));

vect_A[0]->name();   // daje "class A"
vect_A[1]->name();   // daje "class B"
vect_A[2]->name();   // daje "class B"

// Dealokacija se dešava automatski

Pitanje. Kako novi konstruktor dodati klasi std::shared_ptr<T> ?

Oblikovni obrasci (Design Patterns)

Oblikovni obrasci (eng. design patterns) naziv su za rješenja problema koji se često javljaju u konstrukciji koda. Svaki obrazac predstavlja rješenje za jedan oblikovni problem. Opis problema koji obrazac rješava objašnjava nam kada obrazac treba primijeniti; rješenje koje obrazac nudi objašnjava nam kako ćemo ga implementirati u danoj situaciji.

Svaki obrazac ima ime, što je važno za komunikaciju među programerima.

Temeljna literatura o o oblikovnim obrascima je

  • Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides: Design Patterns. Addison-Wesley; 1995.

Obrasci se obično dijele u tri grupe

  • Obrasci stvaranja [Creational Patterns]

  • Abstract Factory, Builder, Factory Method, Object Pool, Prototype, Singleton

  • Strukturni obrasci [Structural Patterns]

  • Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

  • Obrasci ponašanja [Behavioral Patterns]

  • Chain of responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template method, Visitor

Kloniranje objekta koje smo prikazali kao rješenje našeg problema je u stvari primjena oblikovnog obrasca Prototype.

Prototype

Ovaj obrazac koristimo kada želimo kreirati objekt polazeći od danog prototipa.

Struktura

Prototype.png

Sudionici

  • Prototype. Bazna klasa koja deklarira virtualnu funkciju clone().

  • ConcretePrototype. Izvedene klase koje implementiraju funkciju clone() koja kreira objekt tipa klase.

  • Client. Kreira novi objekt tražeći prototip da se klonira.