5.Polimorfism, legare īn momentul executiei si functii virtuale

Asa cum am vazut īn sectiunea 5, C++ este dotat cu proceduri specifice pentru derivarea unor clase dintr-o clasa de baza, pentru utilizarea pointerilor clasei de baza īn adresarea catre obiecte din clasele derivate si pentru manipularea obiectelor derivate īntr-o clasa generica. In ceea ce priveste operatiile permise obiectelor dintr-o asemenea clasa generica, am vazut ca īn clasa de baza sīnt definite actiuni comune tuturor obiectelor derivate (de exemplu, functia getweight() din clasa Vehicle). Daca un pointer al clasei de baza este folosit pentru adresarea unui obiect derivat, tipul pointerului, si nu a obiectului, determina ce functie este apelata, ceea ce duce uneori la rezultate incorecte (vezi cazul clasei Truck). C++ permite si depasirea unor asemenea situatii, deci ca un pointer Vehicle * vp sa apeleze functia Truck::getweight() daca vp pointeaza la un moment dat la un obiect din clasa Truck. Termenul pentru aceasta situatie este polimorfism: pointerul vp ia forme diferite cīnd adreseaza obiecte diferite (cu alte cuvinte, se comporta ca si obiectul pointat). Un alt termen penrtu acest fapt este legarea la momentul executie, si exprima faptul ca decizia care functie este apelata (al clasei de baza sau al clasei derivate) nu se cunoaste la momentul compilarii, ci numai īn timpul executiei.

6.1. Functii virtuale

Regula implicita īn activarea unei functii membru prin intermediul unui pointer este ca tipul pointerului determina functia (vezi lab. 5). Aceasta reprezinta o legare statica (initiala), deoarece tipul functiei este cunoscut īn momentul compilarii. Legarea dinamica (sau ulterioara) este 'implementata' īn C++ cu ajutorul functiilor virtuale. O functie devine virtuala daca declaratia ei īncepe cu virtual. Odata ce o functie a fost declarata virtual īn clasa de baza, definitia ei ramīne virtuala īn toate clasele derivate, chiar daca cuvīntul virtual nu apare explicit. Īn clasa Vehicle doua functii membru vor fi declarate virtual, si anume getweight() si setweight(). Astfel functiile vor deveni virtuale si īn clasa Truck, derivata din Vehicle:

class Vehicle

{

public: // constructori

Vehicle ();

Vehicle (int wt);

virtual int getweight () const; // interfata.. acum virtuala!

virtual void setweight (int wt);

private: // data

int weight;

}

// functia getweight() din clasa Vehicle

int Vehicle::getweight () const

{

return (weight);

}

class Land: public Vehicle

{

.

}

class Auto: public Land

{

.

}

class Truck: public Auto

{

public: // constructori

Truck ();

Truck (int engine_wt, int sp, char const *nm,

int trailer_wt);

void setweight (int engine_wt, int trailer_wt); // interfata: pentru setarea greutatii

int getweight () const; //īntoarce greutatea compusa

private: // data

int trailer_weight;

};

// functia getweight() pentru clasa Truck

int Truck::getweight () const

{

return (Auto::getweight () + trailer_wt);

}

Efectul legarii dinamice este prezentat īn continuare:

Vehicle v (1200); // vehicol cu greutatea de 1200

Truck t (6000, 115,// autocamion avīnd cabina cu greutatea de 6000, viteza 115,

"Scania", 15000); // marca Scania, greutatea remorcii 15000

Vehicle *vp; // pointer generic la vehicle

int main ()

{

// alternativa (1)

vp = &v;

printf ("%d\n", vp->getweight ());

// alternativa (2)

vp = &t;

printf ("%d\n", vp->getweight ());

// alternativa (3)

printf ("%d\n", vp->getspeed ());

return (0);

}

Deoarece functia getweight() este definita ca virtual, legatura se face dinamic: īn instructiunile din alternativa (1), este apelata functia getweight() din Vehicle. Īn alternativa (2) este apelata functia getweight() din clasa Truck. La alternativa (3) va apare o eroare de sintaxa, neexistīnd functia getspeed() īn clasa Vehicle. Regula generala este ca atunci cīnd se utilizeaza un pointer catre o clasa , numai functiile membru ale acelei clase pot fi apelate (fie ca sīnt sau nu virtuale).

Polimorfism īn elaborarea programelor

Cīnd functiile sīnt declarate virtual īn clasa de baza (si deci si īn clasele derivate), si cīnd aceste functii sīnt apelate folosind un pointer catre clasa de baza, acest pointer devine polimorf. Īn continuare vom ilustra efectul polimorfismului īn dezvoltarea si elaborarea programelor. Un sistem de clasificare pentru vehicole poate fi implementat īn C prin intermediul unei uniuni de struct, avīnd si un cīmp enumerare pentru determinarea tipului actual de vehicol reprezentat. O functie getweight() va determina, īn principiu mai īntīi ce tip de vehicol este reprezentat, apoi va inspecta cīmpurile relevante:

typedef enum /* tipul de vehicol */

{

is_vehicle, is_land, is_auto, is_truck,

} Vtype;

typedef struct /* tipul generic de vehicol */

{

int weight;

} Vehicle;

typedef struct /* tipul Land: adauga viteza */

{

Vehicle v;

int speed;

} Land;

typedef struct /* tipul Auto: vehicol Land + nume */

{

Land l;

char *name;

} Auto;

typedef struct /* tipul Truck: Auto + remorca */

{

Auto a;

int trailer_wt;

} Truck;

typedef union /* toate tipurile de vehicole īntr-o uniune */

{

Vehicle v; Land l; Auto a; Truck t;

} AnyVehicle;

typedef struct /* datele pentru toate vehicolele */

{

Vtype type;

AnyVehicle thing;

} Object;

int getweight (Object *o) /* īntoarce greutatea unui vehicol */

{

switch (o->type)

{

case is_vehicle:

return (o->thing.v.weight);

case is_land:

return (o->thing.l.v.weight);

case is_auto:

return (o->thing.a.l.v.weight);

case is_truck:

return (o->thing.t.a.l.v.weight +

o->thing.t.trailer_wt);

}

}

Dezavantajul acestui mod de implementare este ca nu poate fi usor schimbat (de exemplu, daca dorim sa definim tipul Airplane si sa-l adaugam, īmpreuna cu alte cīmpuri (nr. pasageri, etc.), va trebui sa re-editam si sa recompilam codul). Dimpotriva, C++ ofera posibilitatea polimorfismului. Avantajul este ca vechiul cod ramīne utilizabil. Implementarea unui nou tip (Airplane) īnseamna o noua clasa cu (posibil) propriile functii (virtuale) getweight() si setweight(). O functie de forma:

void printweight (Vehicle const *any)

{

printf ("Weight: %d\n", any->getweight ());

}

ramīne valabila oricīnd, si nici nu trebuie recompilata (legatura este dinamica).

Cum este implementat polimorfismul

Īntelegerea implementarii polimorfismului nu este o conditie necesara pentru utilizarea acestei facilitati, dar va explica care este costul, din punct de vedere al memorie, al folosirii lui. Ideea fundamentala a polimorfismului este ca C++ compilerul nu cunoaste, la momentul compilarii, care functie este apelata. Aceasta īnseamna ca adresa functiei trebuie memorata undeva, pentru a fi gasita īn eventualitatea unui apel. Acest 'undeva' trebuie sa fie accesibil obiectelor īn chestiune. Cea mai comuna implementare este urmatoarea: un obiect continīnd functii virtuale pastreaza un prim cīmp ascuns, pointīnd catre un tablou ce pastreaza adresele functiilor virtuale. Trebuie retinut ca aceasta implementare este dependenta de compilator, si nu este dictata de definitiile C++ ANSI. Tabelul adreselor functiilor virtuale este īmpartit cu toate obiectele din clasa. Se poate īntīmpla ca si doua clase sa īmparta acelasi tablou. Cheltuielile de memorie īnseamna:

. un pointer īn plus pentru fiecare obiect, ce pointeaza la:

. un tablou de pointeri pentru fiecare clasa pentru memorarea adreselor functiilor virtuale

O instructiune de genul vp - > getweight() va inspecta mai īntīi cīmpul ascuns al obiectului pointat de vp. Īn cazul sistemului de clasificare al vehicolelor, acest pointer pointeaza catre un tablou cu doua adrese: una pentru functia getweight() si una pentru functia setweight(). Functia actuala care va fi apelata este determinata din acest tablou.

6.2. Functii virtuale pure

Clasa Vehicle contine, īn acest moment, propriile implementari ale functiilor virtuale getweight() si setweight(). Īn C++ este posibil ca o functie sa fie declarata virtuala, īntr-o clasa, fara a mai fi definita concret (implementarea ei facīndu-se īntr-o clasa derivata). Aceasta facilitate, (deci numai declararea, nu si definirea functiilor), este permisa tocmai īn aceasta idee: clasa derivata va trebui sa aiba grija de implementare. Deoarece compilatorul nu permite definirea unui obiect dintr-o clasa care nu contine implementarea concreta, clasa de baza va activa un protocol care va declara functia prin nume, argumente si tipul valorii returnate. Īn acest fel clasa de baza devine un model pentru clasele derivate. Asemenea clase sīnt denumite clase abstracte.

Functiile care sīnt declarate īn clasa de baza dar nu si definite se numesc functii pur virtuale. O functie este declarata pur virtuala prin precedarea declaratiei ei cu cuvīntul virtual si postfixarea cu '= 0'. Exemplu:

class Sortable

{

public:

virtual int compare (Sortable const &other) const = 0;

};

Aici clasa Sortable cere ca toate clasele derivate sa aiba implementate o functie compare(), care sa īntoarca un int si sa aiba ca argument o referinta la un alt obiect Sortable (care pentru a nu fi modificat de functie este declarat const). Nefiindu-i permisa modificarea obiectului curent, functia īnsasi a fost declarata const. Clasa de baza (Sortable) poate fi folosita ca model pentru clasele derivate. Iata un exemplu īn care intervine clasa Person, obtinīndu-se capacitatea de comparare a doua persoane (alfabetic) dupa nume si adresa:

class Person: public Sortable

{

public: // constructori, destructori, redefiniri

Person ();

Person (char const *nm, char const *add, char const *ph);

Person (Person const &other);

Person const &operator= (Person const &other);

// interfata

char const *getname () const;

char const *getaddress () const;

char const *getphone () const;

void setname (char const *nm);

void setaddress (char const *add);

void setphone (char const *ph);

int compare (Sortable const &other) const; // functia impusa de clasa Sortable

private: // data members

char *name, *address, *phone;

};

int Person::compare (Sortable const &o)

{

Person

const &other = (Person const &)o;

register int

cmp;

// daca numele sīnt diferite

if ( (cmp = strcmp (name, other.name)) )

return (cmp);

// daca nu, compara adresele

return (strcmp (address, other.address));

}

Sa observam ca implementarea functiei Person::compare() nu cere ca argument o referinta la Person, ci la un obiect Sortable. Aceasta deoarece, prin posibilitatea de reacoperire a functiilor īn C++, functia compare( Person const &other) este diferita de compare(Sortable const &other), cea ceruta de protocolul activat de clasa de baza. Īn implementarea efectiva vom apela efectiv operatorul cast de conversie pentru a obtine din argumentul de tip Sortable un argument de tip Person.

6.3. Comparerea numai īntre obiecte Person

Uneori este folositor sa cunosti īn implementarea concreta a unei functii virtuale pure ce este argumentul (other). De exemplu, functia Person::compare() ar trebui sa faca comparatia numai daca argumentul este un obiect Person (pentru a evita erorile de executie). De aceea prezentam o versiune īmbunatatita a clasei Sortable, dezvoltata astfel īncīt sa ceara fiecarei clase derivate implementarea unei functii int getsignature():

class Sortable

{

.

virtual int getsignature () const = 0;

.

};

Functia concreta Person::compare() va putea acum compara nume si adrese numai daca semnatura obiectului curent si a obiectului other coincid:

int Person::compare (Sortable const &o)

{

register int

cmp;

// prima data verifica semnaturile

if ( (cmp = getsignature () - o.getsignature ()) )

return (cmp);

Person

const &other = (Person const &)o;

if ( (cmp = strcmp (name, other.name)) )

return (cmp);

return (strcmp (address, other.address));

}

Problema de baza este desigur implementarea functiei getsiganture(). Aceasta va trebui sa īntoarca o valoare unica int pentru fiecare tip de clasa. O implementare eleganta este urmatoarea:

class Person: public Sortable

{

.

int getsignature () const;

}

int Person::getsignature () const

{

static int // variabila specifica pentru clasa Person

tag;

return ( (int) &tag ); // adresa &tag este unica pentru Person

}

6.4. Destructori virtuali

Cīnd operatorul delete elibereaza memoria ocupata de un obiect alocat dinamic, un destructor corespunzator este apelat pentru a asigura ca memoria utilizata pīna atunci va putea fi reutilizata. Sa consideram acum urmatorul cod, īn care sīnt folosite doua clase din sectiunile anterioare:

Sortable *sp;

Person

*pp = new Person ("Frank", "frank@icce.rug.nl", "633688");

sp = pp; // sp pointeaza acum spre un Person

.

.

delete sp; // obiectul este distrus

Īn acest exemplu un obiect din clasa derivata (Person) este distrus folosind un pointer catre clasa de baza (Sortable *). Prin definitie, acest apel va fi efectuat de destructorul clasei de baza, si nu al celei derivate. C++ permite si īn acest caz folosirea destructorilor virtuali, obtinuti prin prefixarea cu cuvīntului virtual. Definitia clasei Sortable devine:

class Sortable

{

public:

virtual ~Sortable ();

virtual int compare (Sortable const &other) const = 0;

.

.

};

Īn general, raspunsul la īntrebarea daca destructorul unei clase trebuie sa fie o functie pur virtuala este nu: nu trebuie fortat faptul ca fiecare clasa derivata sa-si implementeze propriul destructor. Prin definirea destructorului ca virtual, dar nu pur virtual, clasa de baza ofera posibilitatea redefinirii destructorului īn fiecare clasa derivata (dar nu obliga). El va fi folosit doar īn acele clase care nu īsi definesc propriul destructor. De obicei, implementarea destructorului virtual va fi doar o instructiune vida.

6.5. Functiile virtuale si mostenirea multipla

Asa cum am precizat īn laboratorul precedent, este posibil sa deeivam o clasa din mai multe clase de baza. O asemenea clasa va mosteni toate proprietatile claselor parinte. O dificultate majora īn mostenirea multipla poate aparea īn momentul īn care exista mai multe 'drumuri' de la o clasa derivata la o clasa de baza. Iata un exemplu, īn care clasa Derived este derivata dublu din clasa Base:

class Base

{

public:

void setfield (int val) { field = val; }

int getfield () const { return (field); }

private:

int field;

};

class Derived: public Base, public Base

{

};

Datorita dublei derivari, functionalitatea clasei Base apare de doua ori īn clasa Derived. Deci īn momentul unui apel al functiei setfield() de un obiect din clasa Derived, apare ambiguitatea: care din cele doua functii (identice) se vor executa.? Īn aceasta situatie compilatorul va genera eroare.

Cazul de mai sus este simplu de evitat. Dar deoarece mostenirea poate fi facuta īn lant, duplicarea poate fi ascunsa. De exemplu, daca derivam o clasa din clasele Auto si Air, (fie ea AirAuto), ea va contine īn fond doua Vehicle (deci doua cīmpuri weight, doua functii setweight(), etc.).

Ambiguitatea īn mostenirea multipla

Sa vedem de ce clasa AirAuto introduce ambiguitate cīnd este derivata din Auto si Air.

. Un AirAuto este un Auto, deci un Land, deci un Vehicle

. Un AirAuto este un Air, deci un Vehicle

Compilatorul C++ va detecta ambiguitatea īn clasa AirAuto si va genera eroare īn cazul unui cod de forma:

AirAuto cool;

printf ("%d\n", cool.getweight());

Programatorul are doua cai de a rezolva explicit ambiguitatea (care functie getweight() ?)

. Fie va modifica apelul folosind operatorul scope: printf ("%d\n", cool.Auto::getweight ());

Observati locul operatorului scope: īnainte de numele functiei membru

. Fie se poate crea o functie dedicata īn clasa AirAuto, getweight():

int AirAuto::getweight () const

{

return (Auto::getweight ());

}

A doua posibilitate este de preferat, deoarece subliniaza obligatia programatorului de a-si lua masuri speciale de precautie īn cazul ambiguitatilor. Totusi mai exista o solutie eleganta.

Clase de baza virtuale

Putem obtine situatia īn care clasa AirAuto sa contina numai un Vehicle. Acest lucru se obtine asigurīndu-ne ca acea clasa care apare de mai multe ori īn clasa derivata sa fie definita clasa de baza virtuala. Manifestarea unei clase de baza virtuala este urmatoarea: daca clasa B este o clasa de baza virtuala īn clasa derivata D, atunci B poate fi prezenta īn D, dar nu necesar. Compilatorul va ignora includerea daca ea deja a fost facuta. Pentru clasa AirAuto se va modifica derivarea pentru Land si Auto:

class Land: virtual public Vehicle

{

.

.

};

class Air: virtual public Vehicle

{

.

.

};

Derivarea virtuala asigura faptul ca via clasa Land, clasa Vehicle este adaugata numai daca nu a fost deja adaugata (la fel si via Auto).

Observatii privitoare la derivarea virtuala:

. Derivarea virtuala este, spre deosebire de functiile virtuale, o problema de pura compilare: daca derivarea este virtuala sau nu defineste modul īn care compilatorul construieste clasa derivata

. Īn exemplul precedent era suficient ca una din clasele Auto sau Land sa fie construita prin derivare virtuala (īnsa definirea ambelor clase astfel nu conduce la eroare)

. Faptul ca acum clasa Vehicle din AirAuto nu mai este 'inclusa' īn Auto sau Air are consecinte si asupra lantului de constructori. Constructorul lui AirAuto va apela direct constructorul clasei Vehicle (si nu prin intermediul contructorilor claselor Auto sau Air)

Cīnd derivarea virtuala nu este recomandata

Exista īnsa si situatii īn care este recomandata dubla prezenta a unui membru al clasei de baza. Fie urmatorul exemplu:

class Truck: public Auto

{

public: // constructori

Truck ();

Truck (int engine_wt, int sp, char const *nm,

int trailer_wt);

// interfata: pentru setarea celor doua cīmpuri weight

void setweight (int engine_wt, int trailer_wt);

int getweight () const; // returneaza greutatea totala

private: // data

int trailer_weight;

};

// examplu de constructor

Truck::Truck (int engine_wt, int sp, char const *nm,

int trailer_wt)

: Auto (engine_wt, sp, nm)

{

trailer_weight = trailer_wt;

}

// examplu de functie de interfata

int Truck::getweight () const

{

return

( // suma

Auto::getweight () + // camionului plus

trailer_wt // a remorcii

);

}

Definitia arata modul īn care obiectul Truck este construit astfel īncīt sa aiba doua cīmpuri weight: unul de la derivarea din Auto si unul propriu, int trailer_weight. O asemenea definitie este desigur valida, dar poate fi rescrisa. Am putea lasa Truck derivat din Auto si din Vehicle, impunīnd explicit dubla prezenta a clasei Vehicle (unul pentru a fi folosit pentru memorarea greutatii camionului si unul pentru remorca). Dar o derivare de tipul class Truck: public Auto, public Vehicle nu este acceptata de compilator, deoarece Vehicle este deja parte din Auto. Depasirea situatiei se pate face cu ajutorul unei clase intermediare: derivam clasa TrailerVeh din Vehicle si Truck din Auto si TrailerVeh. Toate ambiguitatile referitoare la functiile membru vor fi rezolvate īn clasa Truck:

class TrailerVeh: public Vehicle

{

public: TrailerVeh (int wt);

};

TrailerVeh::TrailerVeh (int wt)

: Vehicle (wt)

{

}

class Truck: public Auto, public TrailerVeh

{

public: // constructori

Truck ();

Truck (int engine_wt, int sp, char const *nm,

int trailer_wt);

// interfata pentru setarea cīmpurilor weight

void setweight (int engine_wt, int trailer_wt);

int getweight () const; // īntoarce greutatea totala

};

// examplu de constructor

Truck::Truck (int engine_wt, int sp, char const *nm,

int trailer_wt)

: Auto (engine_wt, sp, nm), TrailerVeh (trailer_wt)

{

}

// examplu de functie interfata

int Truck::getweight () const

{

return

( // suma dintre

Auto::getweight () + // greutatea camionului plus

TrailerVeh::getweight () // a remorcii

);

}