Reference

Referenca na neki objekt je novo ime za taj objekt. Budući da referenca uvijek mora referirati na neki objekt slijedi pravilo:

Napomena Prilikom deklaracije referenca mora biti inicijalizirana.
int n=100;
int& rn = n;
float ℞ // greška pri kompilaciji. Neinicijalizirana referenca.

Referenca može biti konstantna i tada može referirati na konstantan objekt. Obična referenca ne može referirati na konstantu:

char  &ra = 'a';    // greška pri kompilaciji.
const char &rb = 'b';  // o.k.

Konstantna referenca nam garantira da kroz nju ne možemo promijeniti objekt na koji referira.

Napomena Polje referenci ne postoji.

Deklaracija

double &x[8];  // Polje referenci nije dozvoljeno

je neispravna. Razlog je taj što svaka referenca mora biti inicijalizirana nekim objektom, što pri definiciji polja nije moguće.

Konverzije

Implicitne konverzije

U izrazu sastavljenom od operanada različitih tipova prevodilac prije izvršenja operacije konvertira operanade u zajednički tip. Te konverzije nazivamo implicitnim konverzijama.

Implicitne konverzije se dešavaju u ovim situacijama:

  • Kod aritmetičkih, relacijskih i logičkih izraza.

  • Kod testiranja u if, while, for i do while naredbama dolazi do konverzije u tip bool.

  • Kod izraza pridruživanja (=) dolazi do konverzije u tip varijable na lijevoj strani. Konverzija je legalna i ako pri tome dolazi do gubitka preciznosti.

  • Kod poziva funkcije, ako stvarni i formalni argumenti nisu istog tipa.

  • U return naredbi.

Najčešće su aritmetičke konverzije. Osnovno pravilo je da se konverzija vrši u najširi tip.

Integralna promocija: svi integralni tipovi manji od int (char, signed char, unsigned char, short, unsigned short) pretvaraju se u int, ako je to moguće, a ako ne onda u unsigned int.

Kada se tip bool pretvara u int, onda se true pretvara u 1, a false u nulu.

Ostale konverzije:

  • U većini izraza polje se konvertira u pokazivač na prvi član polja.

  • Nula (0) se može konvertirati u pokazivački tip.

  • Enumeracija se konvertira u integralni tip koji je strojno zavisan.

  • Nekonstantan objekt se može konvertirati u konstantan.

Treba biti posebno pažljiv s izrazima u kojima se pojavljuju signed i unsigned operandi jer C++ dozvoljava da se unsigned varijabli pridruži negativna vrijednost koja se pri tome reinterpretira kao pozitivan broj.

Eksplicitne konverzije

Od prevodioca možemo eksplicitno zahtijevati da napravi konverziju tipova. U C-u bismo to učinili izrazom:

(T) izraz;

gdje je T tip u koji konvertiramo izraz. Jednako je dozvoljen izraz oblika T(izraz). Na primjer:

int x = 3;
int y = 4;
double z = 3.24;
z = x/y; // cjelobrojno dijeljenje
z = double(x)/y; // realno dijeljenje

ili

z = (double) x/y;// realno dijeljenje

ili u C++-u

z = static_cast<double>(x)/y;// realno dijeljenje

C++ uvodi četiri specijalizirana operatora konverzije:

static_cast<T>(izraz);
const_cast<T>(izraz);
dynamic_cast<T>(izraz);
reinterpret_cast<T>(izraz);

static_cast<T> služi za sve one konverzije koje prevodilac radi implicitno, ali ih mi želimo izvršiti "u suprotnom smjeru". Njih moramo zatražiti eksplicitno pomoću static_cast<T>. Na primjer, svaki se pokazivač implicitno može konvertirati u pokazivač na void, ali obratna konverzija se ne dešava automatski. Moguće ju je tražiti eksplicitno:

void *pv = &x;
int *pi;

pi = pv;                     // Greška pri kompilaciji
pi = static_cast<int *>(pv); // o.k.

Ovaj je kod ispravan samo ako je varijabla x kojom smo incijalizirali pokazivač pi tipa int. Prevodilac, općenito, nije u stanju detektirati stvarni tip varijable čija je adresa uzeta. Stoga se greške u upotrebi eksplicitnih konverzija pokazuju za vrijeme izvršavanja programa.

const_cast<T>(izraz) uklanja konstantnost reference ili pokazivača na nekonstantan objekt. Ako objekt nije nekonstantan prevodilac to nije u stanju prepoznati i tada imamo nedefinirano ponašanje.

dynamic_cast<T>(izraz) se koristi za konverziju pokazivača ili reference na baznu klasu u pokazivač ili referencu na izvedenu klasu. To je jedina cast-operacija koja se ne može obaviti klasičnom cast sintaksom. O tom operatoru ćemo više govoriti kod nasljeđivanja klasa.

reinterpret_cast<T>(izraz) vrši konverzije između nepovezanih tipova kao što je konverzija pokazivača u int i obratno. Radi se o reinterpretaciji niza bitova koja ovisi o sustavu na kojem se vrši i stoga nije prenosiva s računala na računalo.

Konverzije i reference

Napomena Nekonstantna referenca nekog tipa može biti inicijalizirana jedino objektom egzaktno tog istog tipa.
double x = 2.71;
int  &rx = x;         // greška
const int  &rx = x;   // o.k

Razlog: Kada prevodilac treba referencu na jedan tip inicijalizirati objektom nekog drugog, kompatibilnog, tipa, on kreira privremeni objekt traženog tipa i inicijalizira referencu tim privremenim objektom. Prevodilac tretira privremeni objekt kao konstantan i stoga zahtijeva konstantnu referencu.

Napomena std::vector<T&> nije dozvoljen zbog toga što reference ne podržavaju kopiranje u standardnom smislu (vrijedi za sve spremnike).

Referenca kao parametar funkcije

Argumenti se funkciji mogu prenijeti po vrijednosti i po referenci.

  • Ako formalni argument funkcije nije referenca dešava se prijenos po vrijednosti: stvarni argument se kopira u formalni;

  • Ako je formalni argument funkcije referenca prijenos je po referenci: formalni argumenti samo referiraju na stvarne.

Primjer. Prijenos po referenci:

void swap(int& x, int& y) {
    int tmp = x;
    x = y;
    y = tmp;
}
Napomena Ako je argument funkcije dekariran kao nekonstantna referenca, onda pri pozivu funkcije nije dozvoljena konverzija tipova za taj argument. Formalni i stvarni argument moraju biti istog tipa.

Na primjer, za funkciju

void print(std::string& text)
{
    std::cout << text << std::endl;
}

imali bismo

std::string a("...");
print(a);      // o.k.
print("...");  // greška

Drugi poziv bi dao grešku pri kompilaciji. Oba poziva bi bila korektna ukoliko bismo funkciju deklarirali na sljedeći način:

void print(const std::string& text)
{
    std::cout << text << std::endl;
}

Referenca na polje

Funkcija koja uzima polje kao argument dobiva pokazivač na prvi element polja. Takva funkcija može uzeti polje bilo koje dimenzije. S druge strane, moguće je deklarirati funkciju koja uzima referencu na polje. Na primjer,

int count(int (&arr)[4])
{
    int x = 0;
    for(int i=0; i< 4; ++i) x += arr[i];
    return x;
}

U toj definiciji ne smijemo zaboraviti dimenziju polja, jer su polja različite dimenzije, ustvari, različitog tipa. Sad možemo pisati,

int arr[]={0,1,2,3};
std::cout << count(arr) << std::endl

No funkcija count će uzimati samo polja dimenzije 4, jer nema implicitnih konverzija između polja različite veličine. Sljedeći kod se stoga ne bi kompilirao:

int arr[]={0,1,2,3,4};
std::cout << count(arr) << std::endl;  // greška

Referenca na polje je dakle manje fleksibilna od pokazivača na polje, pa se stoga rijetko koristi kao parametar funkcije.

Funkcije koje vraćaju referencu

Ako funkcija vraća referencu tada ne dolazi do kopiranja vrijednosti, već samo do kopiranja reference. Za velike objekte to može biti velika ušteda.

Referenca kao povratna vrijednost ima još i tu prednost da predstavlja vrijednost koja se može naći na lijevoj strani znaka jednakosti (lvalue, više o rvalue i lvalue vidi …). To ilustrira ovaj primjer:

char& get(std::string& s, unsigned i)
{
    assert(i < s.size());
    return s[i];
}

Sada mogu promijeniti element u stringu na sljedeći način:

std::string s1("abc");
get(s1,2)='z';

Rezultat je string "abz". Pri tom moramo paziti da nikad ne vratimo referencu na lokalnu varijablu jer će ona nakon vraćanja reference na nju biti uništena. Isto naravno vrijedi i za pokazivač na lokalnu varijablu. Na primjer,

// Neispravan kod
std::string&  ccn(const std::string& s1, const std::string& s2)
{
    std::string tmp = s1+s2;
    return tmp;
}
Napomena Funkcija ne smije nikada vratiti referencu ili pokazivač na lokalnu varijablu.

Lijeve i desne vrijednosti (lvalue, rvalue)

Svakom je izrazu pridruženo jedno svojstvo koje nazivamo lijeva ili desna vrijednost (lvalue ili rvalue).

  • U jeziku C je lijeva vrijednost (lvalue) sve što može stajati s lijeve strane operatora pridruživanja. U jeziku C++ je definicija malo složenija.

  • Svaka varijabla ima lijevu vrijednost (čak i konstantna):

    • int x; x = 3; // o.k

    • const int y = 3; y = 4; // greška Ovdje je y lijeva vrijednost iako ne može stajati na lijevoj strani operatora pridruživanja.

  • Svi objekti s identitetom (lokacijom u memoriji) imaju lijevu vrijednosti.

  • Desnu vrijednost (rvalue) imaju izrazi koji nemaju lijevu vrijednost (ne mogu stajati s lijeve strane operatora pridruživanja).

  • Desna vrijednost je obično bezimeni privremeni objekt, generiran na primjer pri konverzijama, ili eksplicitna konstanta (eng. literal):

    • "ddd" = "ccc"; // greška

    • f(g(x)); Povratna vrijednost funkcije g() je bezimeni privremeni objekt koji se predaje funkciji f().

Lijeve i desne vrijednosti - operatori

Operatori se razlikuju po tome da li zahtijevaju lijeve ili desne vrijednosti kao operande i da li vraćaju lijevu ili desnu vrijednost. Na primjer,

  • Operator pridruživanja mora na lijevoj strani prirodno imati (nekonstantnu) lijevu vrijednost i kao rezultat vraća lijevu vrijednost:

    • (x=y)=z;

  • Adresni operator kao operand traži lijevu vrijednost i vraća desnu vrijednost:

    • int x; &x = 0x2e2f; // greška

  • Operatori dereferenciranja i indeksiranja vraćaju lijevu vrijednost:

    • int *px; *px = 3; px[0] = 2; // o.k.

  • Preinkrement operator vraća lijevu vrijednost, a postinkrement vraća desnu vrijednost:

    • int x = 1; ++x = 7; // o.k. ali ++ nema utjecaja

    • int x = 1; x++ = 7; // greška

  • Operator decltype daje referencu na izrazu koji vraća lijevu vrijednost.

rvalue reference

C++ standard 2011. uvodi novi tip reference - referencu na desnu vrijednost (rvalue reference).

  • Za razliku od obične reference koju označavamo sa &, nova referenca se označava sa &&.

  • Osnovno pravilo je da se (nekonstantna) obična referenca ili referenca na lijevu vrijednost (lvalue reference) ne može inicijalizirati s desnom vrijednošću, dakle s privremenim objektom, objektom koji zahtjeva konverziju (dakle kreira privremeni objekt) ili konstantom.

  • S druge strana referenca na desnu vrijednost se može inicijalizirati samo desnom vrijednošću.

  • Referenca na desnu vrijednost je prije svega referenca - drugo ime za objekt na koji referira.

int i = 1;
int &r1 = i*3;    // greška: i*3 daje desnu vrijednost
int &r2 = 3;      // greška: eksplicitna konstanta je
                  //         desna vrijednost
int &&r3 = 3;     // o.k.
int &&r4 = i*3;   // o.k.
int &&r5 = r4;    // greška: svaka varijabla je lijeva
                  //         vrijednost

Konstantnu referencu, kao i do sada, možemo inicijalizirati s konstantom i privremenim objektom.

int const & r7 = 3; // o.k. kreira privremeni objekt

Desna vrijednost je privremena, obično se nalazi na kraju svog životnog vijeka, i može se slobodno uništiti. Referenca na desnu vrijednost omogućava različito tretiranje lijeve i desne vrijednosti.

Referencu na desnu vrijednost možemo inicijalizirati s lijevom vrijednosti ako koristimo funkciju move iz zaglavlja <utility>:

int &&r6 = std::move(r4); // o.k.

Funkcija std::move služi za eksplicitnu konverziju lijeve vrijednosti u desnu vrijednost. Takvom konverzijom kažemo prevodiocu da objekt (ovdje r4) koji konvertiramo ne namjeravamo više koristiti, osim za destrukciju.

Preopterećenje (overloading) funkcija

  • Dvije funkcije u istom dosegu su preopterećene ako imaju isto ime ali različitu listu parametara. Postojanje više funkcija istog imena nazivamo preopterećenjem funkcije (eng. overloading).

  • Nije moguće preopteretiti dvije funkcije samo na osnovu različitog povratnog tipa.

  • Moguće je preopteretiti samo funkcije u istom dosegu. Kada se funkcije nalaze u različitim dosezima, onda imamo skrivanje funkcije: funkcija iz unutarnjeg bloka skriva funkciju iz vanjskog bloka koja postaje nedostupna u unutarnjem bloku.

    Napomena Preopterećenje je usložnjeno činjenicom da prevodilac vrši konverzije tipova kada stvarni argumenti funkcije po tipu ne odgovaraju formalnim argumentima. Stoga su moguće situacije u kojima za jedan funkcijski poziv postoji više podjednako dobrih funkcija kandidata što predstavljaju grešku pri prevođenju programa.

Nalaženje prave funkcije (overload resolution)

  • Prvi korak: nalaženje kandidata. Prevodilac nalazi sve funkcije danog imena koje su vidljive na mjestu poziva.

  • Drugi korak: selektiranje onih kandidata koji imaju korektan broj parametara i svi parametri se mogu konvertirati u odgovarajući tip. Time dolazimo do skupa dobrih kandidata (viable functions), tj. onih koji mogu biti pozvani sa zadanim parametrima. Prilikom uspoređivanja broja argumenata uzimaju se u obzir i argumenti koji imaju pridruženu vrijednost. Ako je skup dobrih kandidata prazan imamo grešku pri kompilaciji.

  • Treći korak: određivanje najboljeg kandidata. Princip nalaženja je sljedeći: kandidat kod kojeg imamo egzaktno podudaranje argumenta je bolji od kandidata kod kojeg moramo vršiti konverziju argumenta. Kako ima više vrsta konverzija one se rangiraju u svrhu definiranja uređaja među kandidatima (vidi niže). Za funkcije s više argumenata najbolji kandidat je onaj koji nije lošiji od drugih niti po jednom argumentu, a u jednom je bolji od svih. Ako na kraju postupka ima više najboljih kandidata, poziv je dvosmislen i prevodilac javlja grešku.

Nalaženje prave funkcije - primjer

Primjer 1.

void f()                       {cout << "f()"<<endl;}
void f(int x)                  {cout << "f(int)"<<endl;}
void f(int x, int y = 0)       {cout << "f(int,int=0)"<<endl;}
void f(double x, double y=0.0) {cout << "f(double, double=0)"<<endl;}
f(1.2);  // zove f(double, double)

jer f(double, double) ne traži niti jednu konverziju.

Primjer 2. Dvosmislen poziv. Obje su funkcije jednako dobri kandidati.

void f(int x, int y)       {cout << "f(int,int)"<<endl;}
void f(double x, double y) {cout << "f(double, double)"<<endl;}
f(1.2, 2);

Rang lista konverzija prema kvaliteti je sljedeća:

  • Egzaktno podudaranje

  • Integralna promocija

  • Standardna konverzija

  • Konverzija definirana u klasi (class-type conversion)

Preopterećenje na osnovu konstantnosti

Često se funkcija preopterećuje na osnovu konstantnosti (jedna verzija uzima konstantnu referencu, a druga nekonstantnu). To je legalno jer prevodilac uvijek može odrediti koju funkciju pozvati.

void f(int & x)       {cout << "f(int &)"<<endl;}
void f(const int & x) {cout << "f(const int &)"<<endl;}

Tada imamo:

int x = 3;
f(3);   // poziva  f(const int & )
f(x);   // poziva  f(int & )

Kada je const irelevantan, ne može služiti za razlikovanje tipova.

void f(int x) {cout << "f(int)"<<endl;}
void f(const int x) {cout << "f(const int)"<<endl;}// Greška,
                                                   // redefinicija

Konstantni i nekonstantni argument mogu služiti za razlikovanje samo kod referenci i pokazivača.

  • Posve je analogna situacija s pokazivačima na konstantan i nekonstantan objekt.

  • Ne možemo preopteretiti funkciju obzirom na to uzimaju li pokazivač ili konstantan pokazivač (ne pokazivač na const), jer funkcija ionako dobiva kopiju pokazivača.

Preopterećenje na osnovu lijeve/desne reference

Ako želimo razlikovati argument koji ima lijevu vrijednost od onog koji ima desnu trebamo funkciju preopteretiti po lijevoj i desnoj referenci.

void f(const int & ) { std::cout << "f(const int & )\n"; }
void f(int & ) { std::cout << "f(int & )\n"; }
void f(int && ) { std::cout << "f(int && )\n"; }

Sada imamo:

int x = 3;
const int j = 4;
f(3);     // poziva  f(int && )
f(x*x);   // poziva  f(int && )
f(j);     // poziva  f(const int & )
Napomena f(3) selektira desnu referencu jer time izbjegava konverziju desne u (konstantnu) lijevu vrijednost.

Definicija predloška funkcije

Ponekad smo u situaciji da moramo napisati više varijanti jedne te iste funkcije:

char Max(char a, char  b) {
    return a > b ? a : b;
}
float Max(float a, float b) {
    return a > b ? a : b;
}
double Max(double a, double b) {
    return a > b ? a : b;
}

Umjesto da pišemo niz gotovo potpuno istih funkcija možemo napisati jednu parametriziranu funkciju:

template <typename T>
T Max(T a, T b)
{
    return a > b ? a : b;
}

Ovo je predložak funkcije koji prevodiocu kaže da generira specifičnu funkciju kad se javi potreba za njom tako što će parametar T zamijeniti tipom za koji treba generirati funkciju (char, int, float, itd. no naravno to može biti i korisnički tip).

Napomena Predlošci nam dozvoljavaju programiranje neovisno o tipovima.

Upotreba predloška funkcije

Predložak funkcije ima dva tipa parametara:

  • Parametri predloška ( = template parametri; parametar T);

  • Parametri funkcije (koja se dobiva supstitucijom parametara predloška; parametri a i b).

Predložak funkcije u programu koristimo kao i običnu funkciju (parametre predloška ne navodimo).

Napomena Prevodilac iz tipova argumenata predanih funkciji deducira parametre predloška.

Prevodilac iz predloška generira onoliko različitih funkcija koliko je potrebno.

float a=1.0F, b=2.1f;
unsigned char c ='o', d='u';

std::cout  << Max(a,b) << std::endl;
std::cout  << Max(c,d) << std::endl

U prvom primjeru prevodilac će instancirati funkciju Max za parametar T=float, a u drugom za T=unsigned char.

  • Koje će funkcije biti instancirane odlučuje se za vrijeme kompilacije.

Parametri predloška

Parametri predloška spadaju u dvije klase:

  • Mogu biti tipovi i tada se deklariraju s <typename ime_tipa>, pri čemu ključna riječ typename informira prevodilac da je ime_tipa ime nekog tipa.

  • Mogu biti vrijednosti, ali uz niz restrikcija koje se svode na sljedeće:

    • Parametri mogu biti konstantni cjelobrojni izrazi. Tada se deklariraju s <tip_izraza ime_izraza>. Na primjer <int N> gdje je int izraza, a N ime izraza.

    • Parametri mogu biti pokazivači na _objekte/funkcije.

    • Parametri mogu biti lijeve reference na objekte/funkcije.

Argument koji nisu tipovi moraju biti kompilacijski izrazi jer se izračunavaju za vrijeme kompilacije.

1: Najčešće koristimo cjelobrojne vrijednosti:

template <std::size_t dim>
void f(std::array<double, dim> x){
  std::cout <<  "last = " << x[dim-1] <<  std::endl;
}

// ...
std::array<double, 3> x{1,2,3};
f(x);  // dedukcija tipa

2: Primjer s pokazivačem:

template <const char *name>
void f(int x){
  std::cout << name << " : " << x <<  std::endl;
}

const char  hi[] = "Hello!";

int main(){
  f<hi>(2); // dedukcija ovdje nije moguća
  return 0;
}
Napomena Kad je parametar tip onda, radi kompatibilnosti sa starijim kodom, umjesto riječi typename možemo koristiti class.

Dedukcija parametara

Kada prevodilac naiđe na poziv funkcije koja je definirana predloškom on ispituje tip argumenata s kojim je funkcija pozvana kako bi odredio s kojim parametrima treba instancirati predložak. Pri tome se standardne konverzije ne uzimaju u obzir — odnosno preciznije, uzimaju se u obzir samo trivijalne konverzije:

  • Obična referenca/pokazivač bit će konvertirani u referencu/pokazivač na konstantan tip.

  • Polje ili funkcija bit će konvertirani u odgovarajući pokazivač.

Pravilo je dakle, da argumenti moraju egzaktno odgovarati tipovima parametara.

Na primjer,

template <typename T>
T Max(T a, T b)
{
    return a > b ? a : b;
}

poziv tipa

Max(1, 1.2);

vodi do greške pri kompilaciji.

Detalji dedukcije parametra

Općenito dedukcija parametra predloška ima sljedeći oblik (radi jednostavnosti imamo samo jedan parametar):

template <typename T>
void f(ParamType param);

f(expr);

Iz izraza expr prevodilac određuje tip predloška T i tip ParamType.

Primjer: (ParamType = const T &)

template <typename T>
void f(const T & param);

int x;
f(x);

Detalji dedukcije parametra - Slučaj prvi

ParamType = T. Taj slučaj predstavlja prijenos parametra po vrijednosti. Vrijede pravila:

  • Ako je expr referenca na tip, ignoriraj referencu.

  • Ako je izraz nakon zanemarivanja reference konstantan, ignoriraj konstatnost (isto pravilo za volatile).

Primjer:

template <typename T>
void f(T param);

int i = 1;
const int j = 2;
const int & k = j;

f(i);  // T = int
f(j);  // T = int
f(k);  // T = int

Zadatak:

const int * const pi = &j;
f(pi); // T = ?

Detalji dedukcije parametra - Slučaj drugi

ParamType je lijeva referenca. Taj slučaj predstavlja prijenos parametra po referenci. Vrijede pravila:

  • Ako je expr referenca na tip, ignoriraj referencu.

  • Odredi tip T iz tipa izraza expr dobivenog ignoriranjem reference.

Na primjer,

template <typename T>
T f(T& param){ return param;}

int i = 1;
const int j = 2;
const int & k = j;

f(i);  // T = int, ParamType = int&
f(j);  // T = const int, ParamType = const int &
f(k);  // T = const int, ParamType = const int &

Analogno:

template <typename T>
T f(const T& param){ return param;}

int i = 1;
const int j = 2;
const int & k = j;

f(i);  // T = int, ParamType = const int &
f(j);  // T = int, ParamType = const int &
f(k);  // T = int, ParamType = const int &

Detalji dedukcije parametra - Slučaj treći

Univerzalna referenca (forwarding reference): ParamType = T&&. Kada je parametar predloška dan u ovom obliku tada dobivamo ponašanje koje je različito od ponašanja desne reference te stoga govorimo o univerzalnoj referenci. Vrijedi:

  • Ako expr ima lijevu vrijednost onda se T i T&& deduciraju kao lijeve reference.

  • Ako expr ima desnu vrijednost T se deducira na normalan način i rezultirajući tip T&& je desna referenca.

template <typename T>
T f(T&& param){ return param;}

int i = 1;
const int j = 2;
const int & k = j;

f(i);  // T = int&, ParamType =  int&
f(j);  // T = const int&, ParamType = const int &
f(k);  // T = const int&, ParamType = const int &
f(13); // T = int, ParamType = int &&
f(k*k); // T = int, ParamType = int &&

Detalji dedukcije parametra - Složeni kontekst

Izraz ParamType može biti puno složeniji i sadržavati više različitih parametara. Na primjer:

template <typename T, std::size_t dim>
T norm(std::array<T,dim> const & vec){
    T result = 0;
    for(std::size_t i = 0; i < dim; ++i)
       result += vec[i]*vec[i];

    return std::sqrt(result);
}

T i dim moraju se pojaviti u argumentu funkcije kako bi prevodilac mogao selektirati verziju koju će instancirati. Na primjer,

std::array<double,3> vv={1,5,3};
std::cout << norm(vv) << std::endl; // T=double, dim=3
      // instancira norm(std:array<double,3> const &)
Napomena Kvalificirana imena ne mogu poslužiti za dedukciju parametra. Na primjer, iz izraza Q<T>::X tip T neće moćoi biti deduciran.

Konverzije se vrše na običnim argumentima

Ako predložak funkcije ima argument čiji tip nije parametar predloška onda se na njemu vrše standardne konverzije kao kod obične funkcije. Na primjer,

template <typename T>
void h(T t, double r) { r++; cout << r <<","<< t << endl; }

int main() {
    int   z[]={0,1,2,3,4,5};
    char c='q';
    h(z, c);// standardna konverzija u drugom argumentu
    return 0;
}

Moguće je uzeti adresu instance predloška kao u ovom slučaju:

template <typename T>
void f(const T& t) { cout << t << endl; }

void (*pf)(const double&) = f;
  // pf ima adresu instance predloška funkcije f za T=double

Eksplicitno zadavanje parametara

Kada je povratni tip parametar predloška on ne može biti deduciran. Parametri predloška određuju se isključivo prema tipu argumenata funkcije.

template <typename T1, typename T2, typename T3>
T1 sum(T2 x, T3 y) { return x + y; }

Poziv oblika:

double x=7.0;
char c='q';
double u = sum(x,c); // greška

nije legitiman jer prevodilac ne može odrediti T1 iz povratnog tipa. U tom slučaju T1 moramo eksplicitno specificirati:

double u = sum<double>(x,c);

I ostali parametri mogu biti zadani

cout << sum<double>(x,c) << endl;
cout << sum<double,int>(x,c) << endl;
cout << sum<double,int,char>(x,c) << endl;

Parametri koji su eksplicitno zadani odgovaraju redom parametrima kako su poredani u definiciji predloška. To postavlja ograničenje na to koji parametri mogu biti eksplicitno dani: na prva mjesta stavljamo one parametre za koje očekujemo da ćemo ih (eventualno) zadavati eksplicitno.

Napomena Normalne konverzije se vrše na argumentima čiji je parametar predloška eksplicitno zadan.

Preopterećenje funkcijskog predloška

Funkcijski predlošci mogu biti preopterećeni običnim funkcijama ili drugim funkcijskim predlošcima koji se razlikuju po broju argumenata. Na primjer,

#include <iostream>

// Predložak funkcije
template <typename T>
T Max(T const& a, T const& b)
{
    return (a < b) ? b : a;
}

// Preopterećena funkcija
int Max(int const& a, int const& b)
{
    return (a < b) ? b : a;
}

// Preopterećeni predložak
template <typename T>
T Max(T const& a, T const& b, T const& c)
{
    return  Max(Max(a, b),c);
}

Nalaženje prave funkcije (overload resolution)

  • Formira se skup funkcija kandidata koji uključuje:

    • Obične funkcije koje imaju isto ime kao pozvana funkcija;

    • Sve instance svih predložaka funkcija koje imaju isto ime kao pozvana funkcija i za koje je postupak određivanja parametara doveo do slaganja argumenata u instanci i pozivu funkcije.

  • Određuje se koja je od običnih funkcija dobar kandidat, odnosno koja se može pozvati s danim argumentima. Sve instance predložaka su dobri kandidati jer je algoritam određivanja parametara doveo do potpunog slaganja argumenta (do na trivijalne konverzije).

  • Rangiraju se dobri kandidati prema konverzijama koje treba izvršiti da bi se ostvario funkcijski poziv. Pri tome ako je obična funkcija jednako dobra prema konverzijama kao i parametrizirana funkcija, tada se obična funkcija smatra boljim kandidatom.

  • Ako se najbolji kandidat može dobiti instanciranjem nekoliko različitih predložaka bira se "najspecijaliziraniji" predložak.

  • Ako je selektirana kao najbolja samo jedna funkcija, ona se poziva.

  • U svim drugim slučajevima poziv je dvosmislen i program je neispravan.

Prema ovim pravilima vidimo da se obične funkcije preferiraju u odnosu na predloške. Više specijalizirani predlošci se preferiraju u odnosu na manje specijalizirane. Uočimo da je s preopterećenjima lako kreirati dvosmislene situacije pa ih treba koristiti s oprezom.

Koja će funkcija biti pozvana?

// Predložak funkcije
template <typename T>
T Max(T const& a, T const& b) {return (a < b) ? b : a; }

// Preopterećena funkcija
int Max(int const& a, int const& b) {return (a < b) ? b : a; }

// Preopterećeni predložak
template <typename T>
T Max(T const& a, T const& b, T const& c) {return  Max(Max(a, b),c); }


 Max(3,7,1)
 Max(3.0,7.0)
 Max('a','b')
 Max(3,7)
 Max<>(3,7)
 Max<double>(3,7)
 Max(3.0,7)
  • Ako želimo selektirati predložak u slučaju kada bi bila selektirana obična funkcija koristimo prazne oštre zagrade.

  • Osnovna razlika između funkcijskog predloška i funkcije je ta što se pri pozivu funkcije vrše standardne konverzije, dok su kod predloška dozvoljene samo trivijalne konverzije.

  • Standardne se konverzije vrše na argumentima ako je parametar predloška zadan eksplicitno.

Veća ili manja specijalizacija

Sljedeći primjer pokazuje da će prevodilac odabrati predložak f(T*) za T=int, pred jednako mogućim f(T) za T=int *, jer je prvi specijaliziraniji od drugog.

#include <iostream>
#include <typeinfo>

template <typename T>
void f(T t){
  std::cout << "f(" << typeid(T).name() << ")\n";
}

template <typename T>
void f(T * t){
  std::cout << "f(" << typeid(T).name() << "*)\n";
}

int main() {
   int * p = nullptr;
   f(0.0);  // odabire f(T), T=double
   f(0);    // odabire f(T), T=int
   f(p);    // odabire f(T*), T=int
   return 0;
}

Specijaliziranost je parcijalni uređaj i u nekim slučajevim nije moguće naći najspecijaliziraniji predložak.

  • f(T*) je specijaliziraniji od f(T)

  • f(const T*) je specijaliziraniji od f(T*)

  • itd.

Napomena. Predlošci klasa dozvoljavaju specijalizaciju (potpunu i parcijalnu). Predlošci funkcija ne dozvoljavaju specijalizaciju u istom smislu već predlošci oblika f(T*) i f(T) jednostavno preopterećuju jedan drugoga.