Višestruko nasljeđivanje

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

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

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

C c;
A1 * pa1 = &c;
B2 * pb2 = &c;
c.f();    // o.k.
pa1->f(); // o.k
pb2->f(); // greška
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

Pravila o virtualnom nasljeđivanju

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

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.

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

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