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).

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,

rvalue reference

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

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

Nalaženje prave funkcije (overload resolution)

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:

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.

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:

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.

Parametri predloška

Parametri predloška spadaju u dvije klase:

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:

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:

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:

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:

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)

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)

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.

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.