Șablonul de clasă
Un șablon de clasă (class template) permite definirea unor clase ale căror membri pot fi de un tip necunoscut în momentul scrierii codului. Înainte de a defini un șablon de clasă, să analizăm o problemă tipică pe care o putem întâlni și pe care șabloanele o pot rezolva.
Să presupunem că trebuie să definim o clasă care să reprezinte un utilizator, stocând un nume și un id. Tipul id-ului poate fi un număr, un șir de caractere sau alt tip de date. Pentru fiecare variantă, ar trebui să definim o clasă diferită, ceea ce duce la duplicarea codului.
Pentru a evita acest lucru, putem folosi șabloane:
#include <iostream>
template <typename T>
class Person {
public:
Person(T id, std::string name) : id{id}, name{name} { }
void print() const {
std::cout << "Id: " << id << "\tName: " << name << std::endl;
}
private:
T id;
std::string name;
};
int main() {
Person tom{123456, "Tom"}; // T este int
tom.print();
Person bob{"tvi4xhcfhr", "Bob"}; // T este const char*
bob.print();
}
Aici clasa UintPerson reprezintă o clasă de utilizator în care id este un număr întreg de tip unsigned, iar StringPerson este o clasă în care id este un șir de caractere. În funcția main putem crea obiecte din aceste tipuri și le putem folosi cu succes. Deși acest exemplu funcționează, în esență avem două clase identice care diferă doar prin tipul variabilei id. Dar ce se întâmplă dacă va fi nevoie să folosim un alt tip pentru id? Pentru a simplifica codul în C++ putem folosi șabloane de clasă.
Șabloanele de clasă permit reducerea repetiției codului. Pentru a defini un șablon de clasă se folosește următoarea sintaxă:
template <lista_de_parametri>
class nume_clasa
{
// conținutul șablonului de clasă
};
Pentru a folosi șabloanele, înaintea clasei se scrie cuvântul-cheie template, urmat de paranteze unghiulare în care se definesc parametrii șablonului. Dacă sunt mai mulți parametri, aceștia se separă prin virgulă.
Șablonul de clasă, ca și o clasă obișnuită, începe întotdeauna cu cuvântul class (sau struct, dacă este o structură), urmat de numele șablonului și corpul clasei între acolade. Ca și în cazul unei clase obișnuite, șablonul de clasă se termină cu punct și virgulă. Conținutul este practic similar cu o clasă obișnuită, cu deosebirea că în locul unor tipuri concrete, putem folosi parametrii șablonului.
Parametrul din parantezele unghiulare este un identificator arbitrar precedat de cuvântul typename sau class:
template <typename T>
// sau
template <class T>
Aici este definit un singur parametru numit T. Alegerea dintre class sau typename nu are importanță.
Rescriem exemplul cu clasele UintPerson și StringPerson folosind un șablon:
#include <iostream>
template <typename T>
class Person {
public:
Person(T id, std::string name) : id{id}, name{name}
{ }
void print() const
{
std::cout << "Id: " << id << "\tName: " << name << std::endl;
}
private:
T id;
std::string name;
};
int main()
{
Person tom{123456, "Tom"}; // T este un număr
tom.print(); // Id: 123456 Name: Tom
Person bob{"tvi4xhcfhr", "Bob"}; // T este un șir
bob.print(); // Id: tvi4xhcfhr Name: Bob
}
În acest caz, șablonul de clasă folosește un singur parametru T. Adică T va fi un anumit tip, dar nu este cunoscut în momentul scrierii codului.
template <typename T>
class Person {
Parametrul T va reprezenta tipul variabilei id:
T id;
La crearea obiectelor din șablonul Person, compilatorul deduce tipul lui id pe baza primului parametru transmis în constructor. De exemplu:
Person tom{123456, "Tom"};
Valoarea 123456 este un literal de tip int, deci id va avea tipul int.
În al doilea caz:
Person bob{"tvi4xhcfhr", "Bob"};
Valoarea "tvi4xhcfhr" este un literal de tip const char*, deci id va fi de acest tip.
Compilatorul va genera două instanțe separate ale clasei pentru fiecare set de tipuri — una pentru int și alta pentru const char*, și le va folosi pentru a crea obiectele.
În exemplul de mai sus, tipul id este dedus automat. Dar putem specifica explicit tipul în parantezele unghiulare:
int main()
{
Person<unsigned> tom{123456, "Tom"};
tom.print(); // Id: 123456 Name: Tom
Person<std::string> bob{"tvi4xhcfhr", "Bob"};
bob.print(); // Id: tvi4xhcfhr Name: Bob
}
Putem folosi și mai mulți parametri. De exemplu, să definim o clasă pentru o tranzacție bancară:
#include <iostream>
template <typename T, typename V>
class Transaction
{
public:
Transaction(T fromAcc, T toAcc, V code, unsigned sum):
fromAccount{fromAcc}, toAccount{toAcc}, code{code}, sum{sum}
{ }
void print() const
{
std::cout << "From: " << fromAccount << "\tTo: " << toAccount
<< "\tSum: " << sum << "\tCode: " << code << std::endl;
}
private:
T fromAccount; // de la ce cont
T toAccount; // la ce cont
V code; // codul operației
unsigned sum; // suma transferată
};
int main()
{
// tipizare explicită
Transaction<std::string, int> transaction1{"id1234", "id5678", 2804, 5000};
transaction1.print(); // From: id1234 To: id5678 Sum: 5000 Code: 2804
// tipizare implicită
Transaction transaction2{"id6789", "id9018", 3000, 6000};
transaction2.print(); // From: id6789 To: id9018 Sum: 6000 Code: 3000
}
Clasa Transaction folosește doi parametri T și V. Parametrul T definește tipul pentru conturi, iar V definește tipul pentru codul operației — ambele pot fi orice tipuri (numere, stringuri, etc.).
La utilizarea șablonului, trebuie să specificăm cele două tipuri::
Transaction<std::string, int> transaction1("id1234", "id5678", 2804, 5000);
Tipurile sunt transmise în ordinea parametrilor: std::string pentru T, int pentru V.
Pentru transaction2, compilatorul deduce automat tipurile T și V pe baza argumentelor transmise constructorului.
Definirea funcțiilor în afara șablonului de clasă
Sintaxa definirii funcțiilor în afara șablonului de clasă poate diferi puțin de definirea lor în interiorul șablonului. În special, funcțiile definite în afara șablonului de clasă trebuie definite tot ca șabloane, chiar dacă nu utilizează parametri de șablon.
La definirea constructorului în afara șablonului de clasă, numele său trebuie specificat cu numele șablonului de clasă:
#include <iostream>
template <typename T>
class Person {
public:
Person(T, std::string); // constructor obișnuit
Person(const Person&); // constructor de copiere
~Person(); // destructor
Person& operator=(const Person&); // operator de atribuire
void print() const; // metodă a clasei
private:
T id;
std::string name;
};
// definirea constructorului în afara șablonului
template <typename T>
Person<T>::Person(T id, std::string name) : id{id}, name{name} { }
// definirea constructorului de copiere
template <typename T>
Person<T>::Person(const Person& person) : id{person.id}, name{person.name} { }
// definirea destructorului
template <typename T>
Person<T>::~Person(){ std::cout << "Person deleted" << std::endl; }
// definirea operatorului de atribuire
template <typename T>
Person<T>& Person<T>::operator=(const Person& person)
{
if (&person != this)
{
name = person.name;
id = person.id;
}
return *this;
}
// definirea funcției print
template <typename T>
void Person<T>::print() const
{
std::cout << "Id: " << id << "\tName: " << name << std::endl;
}
int main()
{
Person tom{123456, "Tom"};
tom.print();
Person tomas{tom}; // constructor de copiere
tomas.print();
Person tommy = tom; // operator de atribuire
tommy.print();
}
În acest caz, toate funcțiile — inclusiv constructorii, destructorul, operatorul de atribuire — sunt definite ca funcții ale șablonului Person<T>. Deși constructorul de copiere sau funcția print nu folosesc direct parametrul T, ele trebuie în continuare definite ca șabloane. La fel și destructorul.
Parametri șablon cu valori implicite
Asemenea parametrilor funcțiilor, parametrii șabloanelor pot avea valori implicite — un tip care va fi folosit în lipsa unei specificări explicite. De exemplu:
#include <iostream>
template <typename T=int>
class Person {
public:
Person(std::string name) : name{name} { }
void setId(T value) { id = value;}
void print() const
{
std::cout << "Id: " << id << "\tName: " << name << std::endl;
}
private:
T id;
std::string name;
};
int main()
{
Person<std::string> bob{"Bob"}; // T - std::string
bob.setId("id1345");
bob.print(); // Id: id1345 Name: Bob
Person tom{"Tom"}; // T - int
tom.setId(23456);
tom.print(); // Id: 23456 Name: Tom
}
Aici, pentru parametrul șablonului este specificat int ca tip implicit. Parametrul șablonului definește tipul variabilei id, care este setată prin funcția setId.
Putem specifica tipul explicit între parantezele unghiulare:
Person<std::string> bob{"Bob"}; // T - std::string
bob.setId("id1345");
În acest caz, se utilizează explicit tipul std::string, deci id va fi un șir.
În al doilea caz, tipul nu este specificat, deci va fi folosit cel implicit — int:
Person tom{"Tom"}; // T - int
tom.setId(23456);
Prin urmare, id va fi un număr.