Clase si Functii Template

Care-i problema?
Programand intr-un limbaj procedural, adesea ne aflam in situatia de a implementa functionalitati pe care le-am mai implementat de zeci de ori inainte, cum ar fi de exemplu un o lista inlantuita sau o functie de sortare. Incercand sa refolosim ceea ce am implementat deja, constatam ca principalul motiv pentru care nu putem reutiliza codul intocmai il constituie tipul informatiei utile care variaza de la o implementare la alta. Asadar, desi implementarea este cvasi-identica, utilizarea unor liste de string-uri, de numere intregi sau reale va necesita definirea a trei liste diferite, fiecare stocand un alt tip de informatie utila, ceea ce conduce la o nedorita redundanta a codului.

Problema descrisa de mai sus este tocmai cea care a condus la definirea claselor si functiilor template. Aceasta facilitate nu a facut parte din forma initiala a limbajului C++ fiind introdusa de abia in 1990. La ora actuala insa, functiile si clasele template fac parte din propunerea de standardizarea a limbajului C++, fiind considerate unul din puternicele mecanismele de reutilizare a codului oferite de limbaj.

Clase si Functii Template
Pentru a vedea principalele elemente sintactice si semantice specifice claselor/functiilor template sa privim la exemplul unui tablou. Pentru a simplifica exemplul, in cod nu vom face verificarile uzuale din aplicatiile reale.
 

#include <iostream.h>

template <class T>
class CTablou {
public: 
  CTablou(int n = 10) : maxElem(n) { tablou = new[n]; }
  ~CTablou() { delete []tablou; }
  T& operator[] (int index) { return tablou[index]; }
  void AfiseazaTablou();
private:
  T *tablou;
  int maxElem;
};

template <class T>
void CTablou<T>::AfiseazaTablou() {
  for(int i = 0; i< maxElem; i++)
     cout << tablou[i] << " ";
  cout << endl;
}

main() {
  CTablou<float> tFloat(2);
  CTablou<char>  tChar(1);
  CTablou<int>   tInt(1);

  tFloat[0] = 12.35; tFloat[1] = 35.12;
  tChar[0] = 'i';
  TInt[0] = 3;

 tFloat.AfiseazaTablou();
}

Terminologie
In exemplul de mai sus, clasa CTablou este o clasa template, deoarece in declaratia ei am utilizat tipul generic T. La fiecare "instantiere" a unei clase generice suntem obligati sa inlocuim tipul generic T cu un tip concret. In cazul exemplului anterior tipurile concrete folosite au fost float, char si int. Specificarea tipului concret se face sub forma unui parametru (sau argument) al clasei template.

Mecanismul "template" poate fi aplicat atat claselor cat si unor functii globale. Astfel de functii care nu apartin unor clase si care sunt de tip "template" vor fi numite functii template, in timp ce functiile-membru ale unor clase template se numesc metode template.
 

In Spatele Scenei...
Dupa cum se vede din cele de mai sus, tChar este un tablou de caractere, in timp ce tFloat este un tablou de numere reale. Acest lucru se realizeaza simplu pe baza unui mecanism de expandare asemanator macrodefinitiilor. In momentul in care compilatorul intalneste expresia
CTablou<float> el va genera o clasa de tipul CTablou in care va inlocui toate aparitiile tipului T cu float. Astfel, ori de cate ori apare o "instantiere" neexpandata a clasei CTablou, compilatorul va genera o noua clasa.

Avantaje
Exista urmatoarele avantaje majore ale functiilor si claselor template:

  • Scrierea codului se realizeaza intr-un mod foarte natural, perfect asemanator implementarii unei clase obisnuite, partea dificila nefiind rezolvata la programare, ci la compilare.
  • Depanarea se poate face fara nici un fel de probleme, exact ca si in cazul claselor obisnuite.
  • Mecanismul claselor template este o solutie extrem de eficienta pentru implementarea claselor de tip container. Prin specificarea sub forma de parametru a tipului de date ce va fi continut de container vom asigura o eficienta maxima atat din punctul de vedere al celui ce implementeaza clasa -- intrucat are de scris codul o singura data, pentru tipul generic T -- cat si din punctul de vedere al celui ce o utilizeaza -- intrucat acesta nu trebuie decat sa indice pentru fiecare instantiere tipul informatiei utile stocate.
  • Nu exista nici un fel de restrictii legate de tipul generic T. Eventualele inadvertente intre tipul generic si cel concret utilizat la "instantierea" clasei template vor fi semnalate la compilare sau linkeditare.
  • Observatie: Inadvertenta amintita mai sus este cel mai adesea generata de presupunerea ca pentru tipul generic T exista implementati anumiti operatori (==, !=, <, > etc) care insa pentru un anumit tip definit de utilizator sa nu fi fost supraincarcati. Desigur, acest lucru este valabil pentru orice alta functie-membru presupusa definita de posibilele instante concrete ale tipului generic.

    Restrictii legate de Functiile Template
    I. O prima restrictie legata de functiile template este aceea ca in lista de argumente formale trebuie sa apara toate tipurile generice specificate in prefixul template <class T1, . . . , Tn>.  Astfel urmatoarele definitii vor genera erori de compilare:

    template <class T> 
    void f1() { ... }

    template <class T1, class T2>
    T1* f2(T2 t2) { ... } 

    II. A doua restrictie legata de functiile template este aceea ca ele vor fi apelate doar in cazul in care tipurile parametrilor actuali se potrivesc perfect cu cele ale parametrilor formali. Cu alte cuvinte, la apelul functiilor template nu se fac nici un fel de conversii ale parametrilor. Acest lucru este ilustrat in exemplul de mai jos:

    template <class T>
    void f1(T t1, T t2) { ... }

    template <class T1, class T2>
    void f1(T1 t1, T2 t2) { ... } 

    main() {
      f2(1,1);    // corect: T1 si T2 vor fi int. 
      f1(12.34, 56); // eroare: cei doi parametrii trebuie sa fie exact de acelasi tip.
    }


     

    Tipuri Generice ale Functiilor si Claselor Template
    Tipurile concrete cu care sunt substituite tipurile generice ale functiilor si claselor template pot apartine uneia din urmatoarele categorii:

    Dintre cele trei categorii de tipuri, cea mai putin evidenta o reprezinta pointerii la functii. Vom prezenta in continuare un exemplu de utlizare pentru aceasta situatie:
    void f1() { ... }

    template <class T, void(*PointerToFunction)(void)>
    class MyClass {
      T *anAttribute;
    public:
      MyClass() { PointerToFunction(); }
    };

    main() {
      MyClass<int,f1> anObject;
    }

    Expresii constante
    Pe langa cele trei categorii de tipuri concrete mentionate mai sus, pentru clasele template - si numai pentru ele - tipurile generice pot fi substituite si prin expresii constante. Vom ilustra utilizarea expresiilor constante modificand clasa CTablou prezentata mai sus astfel incat dimensiunea maxima a tabloului sa nu mai fie transmisa ca parametru constructorului, ci sa fie specificata ca "parametru" al clasei template:
     

    template <class T, int cMax>
    class CTablou {
    public:
      CTablou() : maxElem(cMax) { tablou = new T[cMax]; }
      // . . .
    private:
      T *tablou;
      int maxElem;
    };
     

    main() {
     CTablou<int, 2> tFloat; 
    }

    Utilizarea tipului void
    In primul rand trebuie spus ca atunci cand o clasa contine un atribut ce are ca tip o clasa template, aceasta clasa este trebuie si ea declarata ca template, dupa cum urmeaza:
     

    template <class T1, class T2>
    class CClass{
      T1 t1;
      T2 t2;
      ...
    };

    template <class T1, class T2>
    class MyClass {
      CClass<T1,T2> myAttrib;
      ...
    };

    S-ar putea ca in unele cazuri, in clasa in care folosim o clasa template, sa nu avem nevoie de toate tipurile generice ale acelei clase. In aceasta situatie, acele tipuri care vor fi nefolosite se vor declara ca void. Astfel,  sa presupunem ca in exemplul de mai sus, clasa MyClass nu foloseste decat un singur tip generic (numit T), care corespunde tipului generic T2. In acest caz, secventa de cod de mai sus va  arata astfel:

    template <class T1, class T2>
    class CClass{
      T1 t1;
      T2 t2;
      ...
    };

    template <class T>
    class MyClass {
      CClass<void,T> myAttrib;
      ...
    };

    Cazuri Exceptate
    Revenim inca o data la exemplul de la inceputul acestei lucrari. Sa presupunem ca dorim sa utilizam clasa template CTablou astfel incat tipul de date stocat in tablou sa fie o clasa dintr-o biblioteca de clase a carei surse ne sunt inaccesibile. Sa presupunem ca afisarea elementelor pentru acest tip de date trebuie sa difere de cea definita in clasa template prin functia Afiseaza Tablou(). Va trebui sa redefinim pentru acest caz functia AfiseazaTablou. In aceasta situatie avem de a face cu un caz exceptat
     

    struct INFO {                  //structura apartine bibliotecii si nu poate fi
      char m_szInfo1[10];          // modificata 
      int  m_cInfo2; 
    } info1 = {"abcde", 12};

    template <class T>
    class CTablou {
    public: 
      // ... 
      void AfiseazaTablou();
    private:
      T *tablou;
      int maxElem;
    };

    template <class T>
    void CTablou<T>::AfiseazaTablou() {
      for(int i = 0; i< maxElem; i++)
         cout << tablou[i] << " ";
      cout << endl;
    }

    void CTablou<INFO>::AfiseazaTablou() {
     for(int i = 0; i < maxElem; i++) {
       cout << tablou[i].m_szInfo1 << "  ";
       cout << tablou[i].m_szInfo2 << " "; 
     }
    }

    main() {
     CTablou<INFO> tINFO(2);
     CTablou<int>  tInt(2);
     tInt.AfiseazaTablou();      // se apeleaza metoda definita pentru cazul generic 
     tINFO.AfiseazaTablou();    // se apeleaza metoda definita pentru cazul exceptat 
    }

    Prin analogie cu cazurile exceptata pentru functii si metode template, limbajul C++ accepta si definirea unor cazuri exceptate de clase, ca in exemplul de mai jos:
    template <class T>
    class MyClass {
     T *myPointer;
    public:
      MyClass() { . . .}
    };

    class MyClass<int> {
     void *myPointer;
    public:
      MyClass<int>() { . . .}
    };

    main() {
      MyClass<int> anObject;     // se foloseste clasa definita pentru cazul exceptat
      MyClass<float> anotherObject;  // se foloseste clasa definita generic.
    }

    Prioritatea Apelului de Functii si Metode
    Intr-un program C++ putem avea trei tipuri de functii avand acelasi nume - f():

    Exemplul de mai jos surprinde cateva aspecte legate de modul in care compilatorul va decide apelarea functiilor cu acelasi nume:
    template <class T> void f(T t) { ... }    // functia template

    void f(int i) { ... }                     // caz exceptat

    float f(float d) { ... }                  // functie supraincarcata

    float f(float d1, float d2) { ... }       // inca o functie suparincarcata 

    main() {
     f('c');                 // functia template 
     f(1);                   // cazul exceptat 
     float d = f(1.2, 3.4);  // functie supraincarcata 
     float dd = f(1.2);      // eroare: s-apelat cazul exceptat 
    }

    Putem deduce din cele de mai sus doua reguli privind prioritatea la apelul unor functii omonime:
            1. Apelul cazurilor exceptate are prioritate fata de functia template generala.
            2. Apelul functiilor supraincarcate nu se va efectua decat daca nu este posibila identificare unei functii template corespunzatoare. Mai mult chiar,  definirea unei functii supraincarcate care ar diferi doar prin tipul valorii returnate (cea de-a patra functie pe care am definit-o) va avea o prioritate mai mica in fata unui caz exceptat al unei functii template.

    Observatie: A doua restrictie legata de functiile template spunea ca pentru aceasta nu se efectueaza nici un fel de convsersii. Aceasta restrictie nu e valabila si pentru cazurile exceptate.
     

    Functii Template si Manipulatori
    In lucrarea nr. 7, la paragraful Manipulatori cu parametri  s-a aratat ca proiectarea acestora necesita definirea unor clase care sa includa printre datele membru variabile avand acelasi tip ca si parametrii manipulatorilor respectivi. Mecanismul claselor generice din C++ permite programatorului sa  defineasca o singura clasa care sa "deserveasca" toti manipulatorii care au acelasi numar de parametri, dar de tipuri respectiv diferite. Astfel, pentru manipulatorii cu un parametru, putem scrie, in  locul clasei manip_int:

    template<class T> 
    class MANIP{
      T i;
      tip_stream& (*f)(tip_stream&, T);
    public:
      MANIP(tip_stream& (*ff)(tip_stream&,T),T ii): f(ff), i(ii){}
      friend tip_stream& operator'op_IO'(tip_stream& s,MANIP& m)
     { return m.f(os,m.i); }
    ;

    iar in locul functiei manip:

    MANIP<tip_par> manip(tip_par p) { 
              return MANIP<tip_par>(func,p); 
    }

    unde: tip_par este tipul real al parametrului necesar manipulatorului, iar func are prototipul:

    tip_stream& func(tip_stream&, tip_par);

    Referindu-ne la exemplul cu setprecision si presupunand ca pentru stream-urile de tip ostream clasa generica prezentata mai sus se numeste OMANIP, avem:
     

    OMANIP<int> setprecision(int i){ 
      return OMANIP<int>(precision,i); 
    }
     unde functia precision ramane cea data in lucrarea 7.
     

    Probleme
    Sa se implementeze prin intermediul claselor template o lista simplu inlantuita si o stiva.
    Detalii de implementare:
      1. Se vor defini urmatoarele trei clase template: CElement, CLista si CStiva.
      2. Clasa CElement va fi clasa ce contine un nod al listei. Intrucat dorim ca lista sa fie optimizata pentru cautarea, informatia continuta va fi bazata pe doua tipuri generice:
                    -     T1 pentru cheia de cautare
                    -     T2 pentru restul de informatii (de fapt informatia cautata).
    (In afara de informatii clasa va contine desigur si pointerul care asigura inlantuirea.)
        3. Clasa CLista va oferi urmatoarele operatii:
                    -    void Insereaza(T1*, T2*);
                    -    CElementLista<T1, T2>* Cauta(T1&);
                    -    int EsteVida();
        4. Clasa CStiva va fi derivata din clasa CLista. Spre deosebire de clasa din care este derivaza, clasa CStiva prezinta particularitatea ca operatia de cautarea nu are sens pentru ea. Prin urmare nu are sens, nici sa o definim ca si clasa template cu doua tipuri generice (T1 si T2), ci intreaga informatie va fi cuprinsa intr-un singur tip generic T, cel de-al doilea parametru ramanand neutilizat . . . deci void!  ;-)
        5. Operatiile definite pentru clasa CStiva sunt cele obisnuite
                    -    void Push(T*);
                    -    T* Pop();
                    -    T* Top();

    Hosted by www.Geocities.ws

    1