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

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

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

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

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.

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
};

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;
}

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;
}

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

Obrasci se obično dijele u tri grupe

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.