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.
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:
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.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.
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>
|
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>
main()
{
|
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:
| void
f1() { ... }
template
<class T, void(*PointerToFunction)(void)>
main()
{
|
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()
{
|
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>
|
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>
|
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>
template
<class T>
void
CTablou<INFO>::AfiseazaTablou() {
main()
{
|
| template
<class T>
class MyClass { T *myPointer; public: MyClass() { . . .} }; class
MyClass<int> {
main()
{
|
Prioritatea
Apelului de Functii si Metode
Intr-un
program C++ putem avea trei tipuri de functii avand acelasi nume
-
f():
| 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()
{
|
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); } |
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();