Supraîncărcare și șabloane

Funcții supraîncărcate

În C++, pot exista două funcții diferite care să aibă același nume dacă listele cu tipurile paramaterilor sunt diferite, fie ca număr de parametri, fie ca tipuri de date. De exemplu:

// functii supraincarcate
#include <iostream>
using namespace std;

int operatie (int a, int b)
{
  return (a*b);
}

double operatie (double a, double b)
{
  return (a/b);
}

int main ()
{
  int x=5,y=2;
  double n=5.0,m=2.0;
  cout << operatie (x,y) << '\n';
  cout << operatie (n,m) << '\n';
  return 0;
}
10
2.5

În acest exemplu, avem două funcții denumite operatie, dar una dintre ele are doi parametri de tip int, în timp ce cealaltă are doi parametri de tip double. Compilatorul știe pe care dintre ele să o apeleze analizând tipurile transmise ca argumente la apelul funcției. Dacă este apelată cu două argumente de tip int, se va executa acea funcție care are parametrii int, iar dacă este apelată cu două valori de tip double, se va executa funcția definită cu parametri de tip double.

În acest exemplu, funcțiile au un comportament diferit: versiunea cu int înmulțește argumentele, în timp ce versiunea cu double le împarte. Aceasta nu este o idee prea bună, în general. De obicei, se așteaptă ca două funcții cu acelasi nume să aibă un comportament asemănător - cel puțin, dar acest exemplu arată că se poate și altfel. Două funcții surpaîncărcate (adică două funcții cu același nume) au definiții complet diferite; întotdeauna, ele sunt două funcții diferite, pentru care doar numele este la fel.

Facem observația că o funcție nu poate fi supraîncărcată numai prin schimbarea tipului returnat. Cel puțin unul dintre parametri trebuie să aibă un tip diferit.

Șabloane de funcții

Funcțiile supraîncărcate pot avea aceeași definiție. De exemplu:

// overloaded functions
#include <iostream>
using namespace std;

int suma (int a, int b)
{
  return a+b;
}

double suma (double a, double b)
{
  return a+b;
}

int main ()
{
  cout << suma (10,20) << '\n';
  cout << suma (1.0,1.5) << '\n';
  return 0;
}
30
2.5

Aici, funcția suma este supraîncărcată cu parametri de tipuri diferite, dar are exact același corp.

Funcția suma ar putea fi supraîncărcată pentru o mulțime de tipuri și tot ar avea sens să își păstreze același corp. Pentru asemenea cazuri, C++ are aabilitatea de a permite definirea de funcții cu tipuri generice, prin ceea ce numim șabloane de funcții. Definirea unui șablon de funcții respectă aceeași sintaxă ca și definirea obișnuită a unei funcții, doar că este precedată de cuvântul cheie template și de o serie de șabloane de parametri incluse între paranteze unghiulare <>:

template <sablon-parametri> declaratie-functie
sablon-parametri reprezintă o serie de parametri separați de virgulă. Acești parametri reprezintă șabloane de tipuri generice dacă precizăm unul dintre cuvintele cheie class sau nume_tip urmat de un identificator. Acest identificator poate fi folosit ulterior în declarația funcției ca un tip de dată obișnuit. De exemplu, o funcție generică suma ar putea fi definită astfel:

1
2
3
4
5
template <class unTip>
unTip sum (unTip a, unTip b)
{
  return a+b;
}

Nu are importanță dacă tipul generic din lista de argumente a șablonului este specificat cu cuvântul cheie class sau cu nume_tip (ele sunt sinonime 100% în șablonul de declarații).

În codul de mai sus, declarația unTip (un tip generic în șablonul de parametri inclus între paranteze unghiulare) permite ca unTip să fie folosit oriunde în definiția funcției, ca orice alt tip; el poate fi folosit ca tip pentru parametri, ca tip returnat sau pentru a declara noi variabile cu acest tip. Întotdeauna, el reprezintă un tip generic ce va fi determinat în momentul ăn care se va instanția șablonul.

Instanțierea unui șablon înseamnă aplicarea lui pentru a crea o funcție ce folosește un anumit tip sau valori pentru parametrii șablon. Aceasta se realizează prin apelarea șablonului funcției, cu aceeași sintaxă ca la un apel obișnuit, dar precizând argumentele șablon între paranteze unghiulare:

nume <argumente-sablon> (argumente-functie)
De exemplu, șablonul pentru funcția suma definit mai sus poate fi apelat astfel:

1
x = suma<int>(10,20);

Funcția suma<int> este doar una dintre posibilele instanțieri ale șablonului de funcție suma. În acest caz, la folosirea lui int în apel, compilatorul creează automat o versiune a funcției suma în care apariția lui unTip este înlocuită cu int, ca și cum ar fi fost definită astfel:

1
2
3
4
int suma (int a, int b)
{
  return a+b;
}

Să vedem un exemplu concret:

// sablon functie
#include <iostream>
using namespace std;

template <class T>
T suma (T a, T b)
{
  T rezultat;
  rezultat = a + b;
  return rezultat;
}

int main () {
  int i=5, j=6, k;
  double f=2.0, g=0.5, h;
  k=suma<int>(i,j);
  h=suma<double>(f,g);
  cout << k << '\n';
  cout << h << '\n';
  return 0;
}
11
2.5

În acest caz, am folosit pe T ca nume de parametru-șablon, în loc de unTip. Nu are importanță, căci T este tot un nume de parametru-șablon pentru tipurile generice.

În exemplul de mai sus, am folosit șablonul funcției suma de două ori. Prima dată cu argumente de tip int, iar a doua oară cu argumente de tip double. Compilatorul a instanțiat și, apoi, a apelat, de fiecare dată, versiunea potrivită de funcție.

De asemenea, să observăm că T este folosit și pentru a declara o variabilă locală cu acel tip (generic) în interiorul funcției suma:

1
T rezultat;

De aceea, rezultat va fi o variabilă de același tip ca și parametrii a și b; la fel și tipul returnat de funcție.
Într-un asemenea caz, în care tipul generic T este folosit ca parametru pentru suma, compilatorul chiar este capabil să deducă automat tipul de date, fără să fie necesară precizarea explicită în interiorul parantezelor unghiulare. De aceea, în loc de explicitarea argumentelor șablonului cu:

1
2
k = suma<int> (i,j);
h = suma<double> (f,g);

este posibil să scriem simplu:
1
2
k = suma (i,j);
h = suma (f,g);

fără să includem tipul între paranteze unghiulare. Evident, pentru aceasta, tipul ar trebui să nu fie ambiguu. Compilatorul ar putea să nu deducă automat tipul lui T, dacă suma este apelată cu argumente de tipuri diferite.

Șabloanele sunt o caracteristică puternică și versatilă. Ar putea avea mai multe șabloane de parametri și, totuși, funcția să poată folosi tipuri obișnuite (nu tipuri șablon). De exemplu:

// sabloane functie
#include <iostream>
using namespace std;

template <class T, class U>
bool sunt_egale (T a, U b)
{
  return (a==b);
}

int main ()
{
  if (sunt_egale(10,10.0))
    cout << "x si y sunt egale\n";
  else
    cout << "x si y nu sunt egale\n";
  return 0;
}
x si y sunt egale

Să observăm că acest exemplu folosește deducerea automată a parametrilor-șablon în apelul lui sunt_egale:

1
sunt_egale(10,10.0)

care este echivalentă cu:

1
sunt_egale<int,double>(10,10.0)

Pentru că, în C++, constantele întregi fără sufixe (precum 10) sunt întotdeauna de tip int, iar constantele reale fără sufix (precum 10.0) sunt întotdeauna de tip double, nu există ambiguitate și, de aceea, argumentele șablon pot fi omise la apel.

Argumente șablon fără tip

Parametrii șablon pot include tipuri nu numai cu class sau nume_tip, ci și expresii de un anumit tip:

// argumente sablon
#include <iostream>
using namespace std;

template <class T, int N>
T inmultire_fixa (T val)
{
  return val * N;
}

int main() {
  std::cout << inmultire_fixa<int,2>(10) << '\n';
  std::cout << inmultire_fixa<int,3>(10) << '\n';
}
20
30

Al doilea argument al șablonului de funcție inmultire_fixa este de tip int. Arată exact ca un parametru de funcție obișnuit, deci poate fi folosit tot așa.

Există, însă, o mare diferență: valorile parametrilor șablon sunt determinate la compilare, pentru a genera o anume instanță de funcției inmultire_fixa. Așadar, valoarea unui asemenea parametru nu este niciodată transmisă în timpul rulării programului. Cele două apeluri ale lui inmultire_fixa din main apelează două versiuni ale funcției: una care dublează, respectiv una care triplează. Exact pentru acest motiv, al doilea argument din șablon trebuie să fie o expresie constantă (nu se poate transmite o variabilă).
Index
Index