int n=100;
int& rn = n;
float ℞ // greška pri kompilaciji. Neinicijalizirana referenca.
Referenca na neki objekt je novo ime za taj objekt. Budući da referenca uvijek mora referirati na neki objekt slijedi pravilo:
|
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.
|
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.
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:
if
, while
, for
i do while
naredbama dolazi do konverzije u tip bool
.
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:
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.
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.
|
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.
|
std::vector<T&> nije dozvoljen zbog toga što reference ne podržavaju kopiranje
u standardnom smislu (vrijedi za sve spremnike). |
Argumenti se funkciji mogu prenijeti po vrijednosti i po referenci.
Primjer. Prijenos po referenci:
void swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
|
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;
}
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.
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;
}
|
Funkcija ne smije nikada vratiti referencu ili pokazivač na lokalnu varijablu. |
Svakom je izrazu pridruženo jedno svojstvo koje nazivamo lijeva ili desna vrijednost (lvalue ili rvalue).
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.
"ddd" = "ccc"; // greška
f(g(x));
Povratna vrijednost funkcije g()
je bezimeni privremeni objekt koji se predaje funkciji f()
.
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,
(x=y)=z;
int x; &x = 0x2e2f; // greška
int *px; *px = 3; px[0] = 2; // o.k.
int x = 1; ++x = 7; // o.k. ali ++ nema utjecaja
int x = 1; x++ = 7; // greška
decltype
daje referencu na izrazu koji vraća lijevu vrijednost.
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 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. |
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:
Č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.
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 & )
|
f(3) selektira desnu referencu jer time izbjegava konverziju desne u (konstantnu) lijevu vrijednost. |
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).
|
Predlošci nam dozvoljavaju programiranje neovisno o tipovima. |
Predložak funkcije ima dva tipa parametara:
T
);
a
i b
).
Predložak funkcije u programu koristimo kao i običnu funkciju (parametre predloška ne navodimo).
|
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 spadaju u dvije klase:
<typename ime_tipa>
, pri čemu ključna riječ typename
informira prevodilac da je ime_tipa
ime nekog tipa.
<tip_izraza ime_izraza>
. Na primjer <int N>
gdje je int
izraza, a N
ime izraza.
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;
}
|
Kad je parametar tip onda, radi kompatibilnosti sa starijim kodom, umjesto riječi typename možemo koristiti class. |
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.
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);
ParamType = T
. Taj slučaj predstavlja prijenos parametra po vrijednosti. Vrijede pravila:
expr
referenca na tip, ignoriraj referencu.
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 = ?
ParamType
je lijeva referenca. Taj slučaj predstavlja prijenos parametra po referenci. Vrijede pravila:
expr
referenca na tip, ignoriraj referencu.
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 &
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:
expr
ima lijevu vrijednost onda se T
i T&&
deduciraju kao lijeve reference.
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 &&
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 &)
|
Kvalificirana imena ne mogu poslužiti za dedukciju parametra. Na primjer, iz izraza
Q<T>::X tip T neće moćoi biti deduciran. |
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
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);
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.
|
Normalne konverzije se vrše na argumentima čiji je parametar predloška eksplicitno zadan. |
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);
}
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.
// 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)
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*)
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.