Višestruko nasljeđivanje

Klasa može proširivati više baznih klasa. U tom slučaju govorimo o višestrukom nasljeđivanju.

  • Objekt izvedene klase sadrži podobjekte svih svojih baznih klasa. Ako klasa proširuje tri bazne klase, onda će instanca te klase sadržavati tri podobjekta baznih klasa. Ta činjenica prestaje vrijediti kada je proširivanje virtualno.

  • Konstruktor proširene klase mora pozvati konstruktore svih svojih baznih klasa. Pri tome će defaultni konstruktori biti implicitno pozvani ukoliko nisu navedeni u inicijalizacijskoj listi. Poredak poziva baznih konstruktora jednak je poretku baznih klasa u derivacijskoj listi.

  • Destruktori se pozivaju u obrnutom poretku od onog kojim su pozvani konstruktori.

mi-1.png

Na primjer, u biblioteci SFML klasa Shape proširuje dvije bazne klase: apstraknu bazu Drawable i implementacijsku bazu Transformable.

classsf_1_1Shape.png

Primjer: Ilustracija konstrukcije izvedenog objekta.

class A1 {
    public:
    A1(){ std::cout << "konstruktor: A1"<< std::endl;}
    void f() {}
    //...
};

class A2 {
    public:
    A2(){ std::cout << "konstruktor: A2"<< std::endl;}
    //...
};

class B1 : public A1, public A2 {
    public:
    B1(){ std::cout << "konstruktor: B1"<< std::endl;}
    void g(){}
    // ...
};
class B2 {
    public:
    B2(){ std::cout << "konstruktor: B2"<< std::endl;}
    void g() {}
    // ...
};

class C : public B1, public B2 {
    public:
    C(){ std::cout << "konstruktor: C"<< std::endl;}
    // ...
};

int main()
{
    C c;

    return 0;
}

Program ispisuje:

konstruktor: A1
konstruktor: A2
konstruktor: B1
konstruktor: B2
konstruktor: C

Primjer - nastavak:

Struktura objekta izvedene klase:

Stablo nasljeđivanja ima ovaj oblik:

visest-nas-1.png

Objekt tipa C ima sljedeću formu:

komponirani-objekt.png
  • Kao i kod jednostrukog nasljeđivanja ako proširena klasa definira konstruktor, onda je njegova dužnost da pozove konstruktore svih svojih baznih klasa. Isto vrijedi za konstruktor kopije, te operator pridruživanja koji moraju brinuti o kopiranju svih podobjekta.

  • Destruktor, kao i kod jednostrukog nasljeđivanja, ne poziva destruktor baznih klasa: o tome brine prevodilac.

Pravila za višestruko nasljeđivanje (uglavnom) su ista kao i za jednostruko

  • Pokazivač ili referenca na instancu proširene klase može biti automatski konvertiran(a) u pokazivač ili referencu na svaku (javnu) baznu klasu. Višestruke konverzije (među kojima prevodilac ne pravi razlike) povećavaju mogućnost dvosmislenih funkcijskih poziva.

C c;
A1 * pa1 = &c;
B2 * pb2 = &c;
  • Bazne klase su međusobno neovisne. Kada objekt koristimo kroz pokazivač/referencu na jednu od baznih klasa, onda nam metode iz drugih baznih klasa nisu dostupne.

c.f();    // o.k.
pa1->f(); // o.k
pb2->f(); // greška
  • Kod višestrukog nasljeđivanja ime koje nije nađeno u proširenoj klasi traži se istovremeno u svim svojim baznim klasama. Ako je ime istovremeno nađeno u dva ili više bazna podstabla onda je poziv dvosmislen. Moguće je u takvoj situaciji precizno specificirati koja se funkcija zove pomoću operatora dosega. Za rješenje dvosmislenog poziva najbolje je implementirati funkciju u proširenoj klasi.

c.g();   // greška
c.B2::g(); // o.k.

Primjer

template <typename T>
class A{
    public:
        A(T x): p(new T(x)) {}
        A(const A& a) : p(new T(a.get())) {
            std::cout << "A-CCtor: T = " << typeid(T).name()
            << ", val = " << a.get() << std::endl;
        }
        A& operator=(const A& a);
        virtual ~A(){
            delete p;
            std::cout << "A-Dtor: T = " << typeid(T).name()<< std::endl;
        }

        T get() const {return *p; }
        void set(T x) { *p = x; }
    private:
        T * p;
};

template <typename T>
A<T>& A<T>::operator=(const A& a)
{
    if( a.p == p ) return *this;

    if( p != 0) delete p;
    p = new T(*a.p);

    std::cout << "A-op=: T = " << typeid(T).name()<< ", val = " << a.get() << std::endl;
    return *this;
}

Primjer (nastavak)

class C : public A<int>, public A<double>
{
    public:
        C() : A<int>(0), A<double>(0)  {}
        C(int x, double y) : A<int>(x), A<double>(y)  {}
        // Poziv funkcijama get i set je dvosmislen.
        // Stoga trebamo nove verzije tih metoda.
        double get_d() { return A<double>::get(); }
        int get_i() { return A<int>::get(); }

        void set_d(double y) { A<double>::set(y); }
        void set_i(int y) { A<int>::set(y); }
};
visest-nas-2.png
int main() {
  A<int> a(2);

  C c;           // defaultni konstruktor
  c.set_i(12);
  c.set_d(16.0);

  A<int> b(c);   // o.k. b se "izrezuje" iz c

  A<double> * p_a = &c; // o.k.  c dohvaćamo kroz
                        // A<double> sučelje
  p_a->set(19.0);
  cout  <<  p_a->get() << endl;  // o.k. Vidi samo funkciju
                                 // get it A<double> klase

  cout << c.get() << endl; // Greška! Dvosmislen poziv.
                           // get postoji u obje bazne klase

  cout <<  c.A<double>::get() << endl; // o.k.
                          // Eksplicitno zadavanje bazne klase
                          // pri pozivu funkcije get.

  C cc(c);  // Sintetizirani CCtor. Zove redom bazne CCtore

  C d;
  d = cc;   // Sintetizirani OP. Zove redom bazne OP

  // ....

  return 0;
}

Kada koristimo višestruko nasljeđivanje?

Višestruko nasljeđivanje prirodno koristimo kada imamo objekt koji implementira više različitih sučelja.

shape.png

Neke baze mijenjaju ponašanje izvedene klase, bez mijenjanja same izvedene klase:

shape-ncopy.png

Obično nasljeđivanje = nevirtualno

Kada nasljeđivanje nije virtualno (ključna riječ virtual nije navedena u derivacijskoj listi) onda u instanci klase svakoj baznoj klasi odgovara jedan podobjekt tipa bazne klase.

struct X{
    int x;
};
struct A : X {
    A(int x_) { x= x_; }
};
struct B : X {
    B(int x_) { x= 2*x_; }
};
struct C : A, B {
    C(int x_, int y_) : A(x_), B(y_)  {}
};

dobivamo:

doubleBase.png
C c(1,2);
c.x = 3; // greška

Klasa koja sadrži dva podobjekta istog tipa ima reduciranu funkcionalnost budući da se iz dosega izvedene klase ne može pozvati funkcija iz bazne klase čije je objekt više puta uključen; Svaki takav poziv je dvosmislen.

Virtualno nasljeđivanje

Kada je u derivacijskoj listi bazna klasa X deklarirana virtual (virtualna baza) onda će svaka klasa izvedena iz nje sadržavati samo jedan podobjekt tipa X bez obzira koliko se puta u stablu nasljeđivanja tip X javlja kao virtualna baza.

Prethodni primjer s virtualnim nasljeđivanjem daje:

struct X{
    int x;
};
struct A : virtual X {
    A(int x_) { x= x_; }
};
struct B : virtual X {
    B(int x_) { x= 2*x_; }
};
struct C : A, B {
    C(int x_, int y_) : A(x_), B(y_)  {}
};
virtualBase.png
int main(){

    C c(1,2);
    c.x = 3; // o.k. samo je jedan x

Primjer iz standardne biblioteke

virtual_stl.png

Ista klasa može biti virtualna i nevirtualna baza

Svako virtualno pojavljivanje klase X u stablu nasljeđivanja odgovara jednom podobjektu, dok svako drugo, nevirtualno, pojavljivanje iste klase X rezultira zasebnim podobjektom tipa X. Na primjer,

class X { /* ... */ };
class A : public virtual X { /* ... */ };
class B : public virtual X { /* ... */ };
class C : public  X { /* ... */ };
class D : public A, public B, public C { /* ... */ };

ima dijagram podobjekata ovog oblika:

BartonNackmanE-1.png
  • Klasa koja ima virtualnu baznu klasu deklarira da je spremna dijeliti svoju virtualnu bazu u stablu nasljeđivanja. Svaki objekt nasljeđuje točno jedan podobjekt dane virtualne bazne klase bez obzira na to koliko se puta bazna klasa javlja kao virtualna baza u stablu nasljeđivanja.

Pravila o virtualnom nasljeđivanju

  • Objekt izvedene klase može se koristiti kroz referencu ili pokazivač tipa virtualne bazne klase na isti način kao što se koristi kroz referencu/pokazivač tipa regularne bazne klase.

  • Svaki član virtualne baze može se dohvatiti bez dvosmislenosti poziva budući da postoji samo jedan podobjekt virtualne baze.

  • Kod virtualnog nasljeđivanja nastaje situacija u kojoj do članice virtualne bazne klase ima više staza kroz stablo nasljeđivanja. Pri tome su moguće tri situacije:

    • Ako svaka staza vodi do istog člana u virtualnoj bazi onda je poziv korektan.

    • Ako jedna staza vodi do člana u virtualnoj bazi, a druga do člana prerađenog u nekoj od klasa izvedenih iz virtualne baze, onda je ponovo poziv korektan i poziva se prerađena metoda koja dominira.

    • Ako kroz različite staze dolazimo do različitih prerađenih članova u klasama koje proširuju virtualnu bazu, onda je poziv dvosmislen.

Primjer:

class V { public: virtual int f(){ return 2; } int x; };
class W { public: int g(); int y; };
class B : public virtual V, public W
{
public:
    int f(){ return 5; }  int x;
    int g();  int y;
};
class C : public virtual V, public W { };
class D : public B, public C { void h(); };
virtual_inheritance.png
void D::h()
{
    x++;    // OK: B::x skriva V::x
    f();    // OK: B::f() skriva V::f()
    y++;    // greška: B::y ili C::W::y ?
    g();    // greška: B::g() and C::W::g() ?
}
int main()
{
    D d;
    V * pv = &d;
    std::cout << pv->f() << std::endl;   // 2 ili 5 ?
    return 0;
}

Konstrukcija klase s virtualnom bazom

Ako virtualna bazna klasa mora biti konstruirana, onda imamo problem: kod regularnih baza dužnost je izvedene klase da pozove konstruktore svih svojih baznih klasa, ali to nije primijenjivo na virtualne baze jer bismo tada dobili više podobjekta koji odgovaraju višestrukom pojavljivanju virtualne baze u stablu nasljeđivanja.

Problem konstrukcije virtualne baze riješava se sljedećim pravilom: Konstruktor virtualne baze u svojoj inicijalizacijskoj listi poziva konstruktor najizvedenije klase u stablu nasljeđivanja. Ilustrirajmo to sljedećim primjerom:

Primjer:

class A{
    int x;
    public:
    A(int _x) : x(_x) {cout << "A("<<_x<<")"<<endl;}
    int get_x(){ return x; }
    // ...
};

class B : public virtual A{
    int y;
    public:
    B(int _y) : A(10 * _y), y(_y) {cout << "B("<<_y<<")"<<endl;}
    int get_y(){ return y; }
};

class C : public virtual A{
    int y;
    public:
    C(int _y) : A(_y*_y), y(_y) {cout << "C("<<_y<<")"<<endl;}
    int get_y(){ return y; }
};

class D : public B, public C{
    public:
        D(int _x, int _y) : A(_x), B(_y), C(_y) {cout << "D("<<_x<<","<<_y<<")"<<endl;}
};

Dijagram klasa je sljedeći:

virt_inh_base.png
  • Klasa D, kao najizvedenija, poziva konstruktor svoje virtualne baze.

  • Klase B i C također moraju u svojim konstruktorima inicijalizirati A kao svoju direktnu bazu, ali prevodilac te inicijalizacije ignorira.

  • Ukoliko najizvedenija klasa ne pozove konstruktor virtualne baze prevodilac će ubaciti poziv defaultnom konstruktor; ako takvog nema javlja se greška pri kompilaciji.

Primjer

Izvršimo li sljedeći program:

int main()
{
    D d(2,6);

    cout << d.get_x() << endl;
    cout << d.C::get_y() << endl;

    return 0;
}

dobivamo ispis:

A(2)
B(6)
C(6)
D(2,6)
2
6

Vidimo da se prvo konstruira virtualna baza, a zatim ostali dijelovi objekta, prema redosljedu pojavljivanja u derivacijskoj listi. To je općenito pravilo:

  • Virtualne baze se konstruiraju prije nevirtualnih baza, bez obzira na mjesto pojavljivanja u stablu nasljeđivanja.

O implementaciji višestrukog i virtualnog nasljeđivanja

Jednostruko nasljeđivanje je na određen način prirodno:

class Base{
  int x;
};

class Derived : public Base{
  int y;
}

Derived * pDerived = new Derived();
Base *    pBase = pDerived;

Objekt klase Derived u memoriji ima oblik:

simple-1.png

Pokazivači pBase i pDerived: pokazuju na istu memorijsku lokaciju.

Ako su klase polimorfne onda se instanca klase povećava za pokazivač na tabelu virtualnih funkcija.

class Base{
public:
    virtual void set(int x_){ x = x_; }
protected:
  int x;
};

class Derived : public Base{
public:
  void set(int x_) { x= x_*x_; }
  int y;
};

a objekti u memoriji

simple-2.png

Svaki objekt sada ima pokazivač na virtualnu tabelu. Novi element u virtualnoj tabeli, koji do sada nismo spominjali, je pokazivač na typeinfo objekt. To je još jedna tabela pridružena klasi koja sadrži informacije o klasi za RTTI sustav. Pokazivač na nju se smješta u tabelu virtualnih funkcija.

O implementaciji …

Višestruko nasljeđivanje je složenija.

class BaseA{
public:
  virtual void set(int x_){ x = x_; }
  int x;
};

class BaseB{
public:
  virtual int get(){ return y; }
  int y;
};

class Derived : public BaseA, public BaseB{
public:
  int z;
  virtual void square(){ z = z*z; }
};
Derived * pDerived = new Derived();
BaseA *    pBaseA = pDerived;
BaseB *    pBaseB = pDerived;

U ovoj situaciji objekt klase Derived u memoriji i pripadna tabela virtualnih funkcija imaju sljedeći oblik:

simple-3.png

Kao i do sada pokazivači pDerived i pBaseA drže istu adresu i pokazuju na početak objekta. S druge strane, pokazivač pBaseB (BaseB je desna baza u derivacijskoj listi) ima različitu adresu od one u pDerived — on pokazuje na podobjekt BaseB u objektu tipa Derived.

  • Kao posljedicu imamo da kod višestrukog nasljeđivanja objekt ima po jedan pokazivač na virtualnu tabelu za svaku svoju bazu. I sam broj virtualnih tabela je jednak broju baza, s time da se u nekim implementacijama one spoje u jednu tabelu (kao u našem prikazu).

  • Druga posebnost višestrukog nasljeđivanja je potreba za korekcijom this pokazivača pri pozivu virtualnih funkcija kroz pokazivač na desnu baznu klasu. Iz pokazivača na podobjekt desne bazne klase potrebno je dobiti pokazivač na početak cijelog objekta. Tu korekciju ne može napraviti prevodilac za vrijeme kompilacije jer je dinamički tip pokazivača poznat tek za vrijeme izvršavanja programa. Stoga virtualna tabela može pored pokazivača na virtualne funkcije sadržavati i pomake (offsets) potreban da se dođe do početka virtualne tabele (odnosno vtb pridružene lijevoj bazi).

  • Situacija postaje puno složenija kod virtualnog nasljeđivanja. Prije svega, podobjekt virtualne bazne klase dolazi nakon svih ostalih dijelova objekta. To znači da se njegova pozicija u klasama nastalim nasljeđivanjem klase s virtualnom bazom mijenja, pa mu se mora pristupati indirektno. Virtualnom baznom dijelu može se pristupiti putem pokazivača na bilo koju njenu podklasu te stoga na osnovi tog pokazivača nije jasno gdje se virtualna baza nalazi — ta se informacija mora pročitati iz virtualne tabele. Nadalje, postupak konstrukcije i destrukcije objekta s virtualnom bazom postaje složeniji jer ne smije svaki konstruktor podklase konstruirati svoju virtualnu bazu. To mora napraviti samo jedan od njih, te isto tako, pri destrukciji samo jedan destruktor uništava virtualnu bazu. Zbog svih tih komplikacija prevodilac generira više različitih virtualnih tabela koje mu omogućavaju da izvrši svoju zadaću.

Adapter

Ponekad imamo klasu koja nam nudi funkcionalnost koja nam je potrebna, ali nema odgovarajuće sučelje. U tom slučajukoristimo oblikovni obrazac Adapter.

Struktura obrasca je sljedeća:

adapter.png
  • Klijent ima referencu na Target i koristi sučelje klase Target;

  • Traženu funkcionalnost daje klasa Adaptee, ali ona nema traženo sučelje.

  • Konstruiramo klasu Adapter koja:

    • javno nasljeđuje klasu Target i stoga ima sučelje klase Target;

    • privatno nasljeđuje klasu Adaptee koju koristi za implementaciju svog sučelja.

Umjesto privatnog nasljeđivanja možemo uvijek koristiti agregaciju:

adapter-1.png

Adapter - primjer

Imamo klasu Graphics koja koristi SFML biblioteku za iscrtavanje ravninske triangulacije.

Screenshot_airfoil.png
class Graphics{
    public:
        Graphics(SFMLGrid * pGrid, std::string const & fileName);
        void run();
    private:
        sf::RenderWindow mWindow;
        SFMLGrid * mGrid;

        void processEvents();
        void update();
        void render();
};

Triangulaciju pamti klasa SFMLGrid u obliku sf::VertexArray objekta koji sadrži sve stranice triangulacije. Objekti te klase se znaju iscrtati.

class SFMLGrid : public sf::Drawable, public sf::Transformable{
    public:
        SFMLGrid();
        virtual void readFromFile(std::string const & fileName);
    private:
        sf::VertexArray mVA;
        void draw(sf::RenderTarget & target, sf::RenderStates states) const override;
};

Imamo klasu GmshGrid koja može pročitati mrežu generiranu pomoću gmsh alata za generiranje triangulacije.

class GmshGrid{
  public:
    using Triangle = std::tuple<std::size_t, std::size_t, std::size_t>;
    GmshGrid();
    void read(const std::string & fileName);
    // ...
  private:
      std::vector<double>   mVertexXcoor;
      std::vector<double>   mVertexYcoor;
      std::vector<Triangle> mElement;
};

Ova klasa koristi drugačije sučelje, drugačiju strukturu podataka za pamćenje mreže i ne zna se iscrtavati.

Konstruiramo klasu Adapter koja adaptira GmshGrid na sučelje a SFMLGrid klase:

class Adapter: public SFMLGrid, private GmshGrid{
public:
    Adapter();
    void readFromFile(std::string const & fileName);
private:
   // ...
};

Klasa Adapter prerađuje metodu readFromFile() koristeći GmshGrid::read() te radi svu potrebnu adaptaciju.

adapter-2.png