Operatori su funkcije
Operatori su posebna vrsta funkcija čije se ime sastoji od ključne riječi operator iza koje slijedi simbol operacije.Na primjer,
Vect operator+(const Vect&, const Vect&);
Broj parametara operatora je jednak broju operanada, pri čemu treba računati
i implicitni this
parametar.
Sljedeći operatori se mogu preopteretiti:
+ |
- |
* |
/ |
% |
^ |
& |
| |
~ |
! |
, |
= |
< |
> |
<= |
>= |
++ |
— |
<< |
>> |
== |
!= |
&& |
|| |
+= |
-= |
*= |
/= |
%= |
^= |
&= |
|= |
< = |
> = |
[] |
() |
→ |
→* |
new |
new [] |
delete |
delete [] |
Operatori koji ne mogu biti preopterećeni:
:: |
.* |
. |
?: |
Pravila
-
Nije moguće kreiranje novih operatora spajanjem simbola (na primjer
**
). -
Možemo preopteretiti samo već postojeće operatore.
-
Barem jedan operand mora biti tipa klase (ili enumeracije): drugim riječima, ne možemo promijeniti ponašanje operatora na ugrađenim tipovima:
int operator+(int, int); // Greška.
Napomena
|
Prioriteti, asocijativnost i broj operanda fiksirani su za svaki operator i prilikom preopterećenja ne mogu se mijenjati. |
Četiri operatora (+
, -
, *
, &&
) mogu biti unarni i binarni. Svaki se od njih može
preopteretiti, a koji je definiran ovisi o broju parametara.
Defaultni argumenti kod operatora nisu dozvoljeni, osim kod operatora funkcijskog
poziva: operator()
.
Napomena
|
Preopterećeni operatori ne garantiraju poredak izračunavanja operanada.
Posebno, lijeno izračunavanje logičkih izraza ne prenosi se na preopterećene
logičke operande && i || . Oba argumenta tih operatora se uvijek izračunavaju;
operator zarez (,) preopterećen ne garantira poredak izračunavanja.
Stoga vrijedi pravilo: Nikad ne preopteretiti operatore && , || i zarez. |
Operator može i ne mora biti član klase
Napomena
|
Operator član klase prima implicitni parametar this koji je vezan uz prvi operand. |
Aritmetički i relacijski operatori se obično definiraju izvan klase, dok se operatori pridruživanja redovito implementiraju kao funkcije članice.
// lijevi operand je dan implicitnim this pokazivačem
Vect& Vect::operator+=(const Vect&);
// Nije članica klase -- ima dva parametra
Vect operator+(const Vect&, const Vect&);
Uočimo da složeni operator vraća referencu kako bi povratna vrijednost mogla služiti kao vrijednost na lijevoj strani.
Ako je operator definiran izvan klase, a mora dohvatiti privatne članove klase (tipično operatori ulaza/izlaza), onda se on definira kao prijatelj klase.
Upotreba preopterećenog operatora
Sintaksa je vrlo prirodna, odnosno ista kao i za ugrađene tipove. Ako su npr.
A1
i A2
tipa Vect
, onda ispis njihovog zbroja izgleda ovako:
std::cout << A1 + A2 << std::endl;
Ekvivalento bismo mogli pisati:
cout << operator+(A1, A2) << endl;
Analogno,
A1 += A2; // prirodna sintaksa
A1.operator+=(A2); // ekvivalentan funkcijski poziv
su dvije posve ekvivlentne naredbe.
Operator član klase ili globalna funkcija
-
operatori pridruživanja (
operator=
), indeksiranja (operator[]
), funkcijski poziv (operator()
) i strelica (operator->
) moraju biti metode članice (zahtjev prevodioca). -
Složena pridruživanja trebaju biti metode članice, ali nije greška kompilacije ako nisu.
-
Inkrement, dekrement, dereferenciranje, … u većini slučaja su metode članice.
-
Simetrični operatori, aritmetički, relacijski, jednakost, itd. trebaju biti globalne funkcije.
Operatori ulaza/izlaza
Preopterećenje operatora <<
Konzistentnost s IO bibliotekom zahtjeva da operator <<
-
Uzima
ostream&
kao prvi parametar; -
Uzima referencu na konstantan objekt tipa klase kao drugi parametar;
-
Vraća referencu na svoj ostream parametar.
Napomena
|
Operator << i >> moraju uvijek biti implementirani izvan klase. |
Razlog je jednostavan. Kada je operator član klase njegov prvi argument (argument na lijevoj strani operatora)
je implicitni this
pokazivač, koji pokazuje na objekt na kome se nalazimo. To ima za posljedicu
da bi ispis objekta klase Point3D
u kojoj je definiran operator <<
izgledao ovako:
Point3D A;
// .....
A << std::cout; // Ekvivalentno s A.operator<<(std::cout)
Ispis objekta Point3D
template <typename T>
std::ostream & operator<<(std::ostream & out, Point3D<T> const & p){
out << "["<< p.data[0] <<"," << p.data[1] << "," << p.data[2] << "]";
return out;
}
Operator ispisa dohvaća privatne podatke klase pa stoga mora biti deklariran kao friend
.
Klasa Point3D
, u ovom trenutku, izgleda ovako:
template <typename T>
class Point3D
{
public:
explicit Point3D(T x=0, T y=0, T z=0);
// ...
// friend deklaracija operatora
template <typename U>
friend std::ostream & operator<<(std::ostream &, Point3D<U> const &);
private:
T data[3];
// ...
};
Preopterećenje operatora >>
Konzistentnost s IO bibliotekom zahtijeva da operator >>
-
Uzima
istream&
kao prvi parametar. -
Uzima nekonstantnu referencu na objekt tipa klase kao drugi parametar.
-
Vraća referencu na svoj istream parametar.
template <typename T>
std::istream & operator >>(std::istream & in, Point3D<T> & p){
T data[3]; // pomoćna varijabla
for(unsigned int i = 0; i<3; ++i)
{
in >> data[i];
if(in.fail())
throw std::runtime_error("Operator >> : incorrect input.");
}
// Ne korumpiramo postojeće podatke ako je došlo do greške
// pri učitavanju.
for(unsigned int i = 0; i<3; ++i) p.data[i] = data[i];
return in;
}
-
Operator u mnogim situacijama ima vrlo malu mogućnost tretiranja grešaka. U našem slučaju izbacujemo izuzetak.
-
Koristimo pomoćnu varijablu da izbjegnemo djelomičnu promjenu varijable
p
u slučaju greške. -
Ulazni stream treba dovesti u ispravno stanje pozivni program u kojem se hvata izuzetak.
Aritmetički i relacijski operatori
Aritmetičke i relacijske operatore definiramo kao funkcije nečlanice. U svrhu primjera
dodat ćemo našoj klasi Point3D
operator zbrajanja:
template <typename T>
Point3D<T> operator+(Point3D<T> const & p1, Point3D<T> const & p2){
Point3D<T> tmp(p1.data[0]+p2.data[0],
p1.data[1]+p2.data[1],
p1.data[2]+p2.data[2]);
return tmp;
}
Posve analogno bismo konstruirali operator oduzimanja. Uočimo da naš operator kreira privremeni
objet koji zatim vraća u return
naredbi. To je prilično skupa operacija
jer u kodu oblika:
Point3D<double> a,b,c;
// .....
c = a + b;
Pozivamo konstruktor za privremeni objekt, operator pridruživanja i destruktor. Da bismo optimizirali ponašanje klase treba implementirati konstruktor kopije i operator pridruživanja premještanjem.
Operatori ==
i !=
se implementiraju na isti način, ali u našem slučaju imaju smisla samo
za neke tipove T
(na primjer T=int
) pa ih nećemo implementirati.
Složena pridruživanja
Složene operatore +=
i -=
moguće je implementirati bez konstrukcije
privremenog objekta te su oni stoga efikasniji.
template <typename T>
Point3D<T>& Point3D<T>::operator+=(const Point3D& rhs)
{
data[0] += rhs.data[0];
data[1] += rhs.data[1];
data[2] += rhs.data[2];
return *this;
}
Zadatak: Da li je dozvoljen sljedeći kod:
(a += b) = c;
?
Skaliranje točke možemo postići operatorom *=
koji uzima skalarni argument.
template <typename T>
Point3D<T>& Point3D<T>::operator*=(T const & scalar)
{
data[0] *= scalar;
data[1] *= scalar;
data[2] *= scalar;
return *this;
}
Operator pridruživanja (kopiranjem i drugi)
Operatori pridruživanja koji uzimaju referencu tipa klase su istaknuti među svim mogućim drugim operatorima pridruživanja i stoga imaju posebno ime: operator pridruživanja kopiranjem/premještanjem (copy assignment operator, move assignement operator). Na primjer,
Vect& operator=(const Vect& v);
Vect& operator=(Vect&& v);
Ti operatori pridruživanja mogu biti preopterećeni, tj. možemo definirati druge operatore pridruživanja koje uzimaju argumente drugog tipa. Pri tome imamo ograničenje:
-
Svaki preopterećeni operator pridruživanja mora biti član klase;
-
Svaki preopterećeni operator pridruživanja uzima samo jedan argument.
Implementacija novog operatora pridruživanja u klasi Point3D
bi mogla biti ta da svakoj komponenti
vektora pridružuje danu vrijednost:
Point3D& Point3D::operator=(Real x)
{
data[0] = x;
data[1] = x;
data[2] = x;
return *this;
}
Operator indeksiranja []
Klase koje predstavljaju spremnike obično definiraju operator indeksiranja operator[]
kroz koji se dohvaćaju elementi spremnika.
Napomena
|
Operator indeksiranja mora biti član klase i uzima točno jedan parametar. |
Na primjer, sve get metode u klasi Point3D
mogli bismo zamijeniti jednim operatorom:
template <typename T>
class Point3D
{
T operator[](unsigned int i) const{ return data[i]; }
T& operator[](unsigned int i) { return data[i]; }
// .....
};
Sada možemo pisati:
Point3D a(2,2,2);
double x = a[1]; // x = a.operator[](1)
a[1]=2.0; // o.k.
Nužno je da operatori vraćaju referencu i da su preopterećeni po konstantnosti.
Zadatak. Ubacite kontrolu granica indeksa u operator indeksiranja.
Operator indeksiranja mogućava dohvat elemenata izvan klase i eliminira potrebu za friend deklaracijama. Možemo na primjer pisati:
template <typename T>
Point3D<T> operator+(Point3D<T> const & p1, Point3D<T> const & p2){
Point3D<T> tmp(p1[0]+p2[0],
p1[1]+p2[1],
p1[2]+p2[2]);
return tmp;
}
Parametrizirana klasa Point3D<T>
// Datoteka point3D.h
#ifndef _POINT3D_H_IS_INCLUDED_
#define _POINT3D_H_IS_INCLUDED_
template <typename T>
class Point3D
{
public:
explicit Point3D(T x=0, T y=0, T z=0);
T operator[](unsigned int i) const{ return data[i]; }
T& operator[](unsigned int i) { return data[i]; }
Point3D& operator+=(const Point3D& rhs);
Point3D& operator-=(const Point3D& rhs);
Point3D& operator *=(T const & t);
Point3D & translate(const Point3D& dir);
Point3D & rotate(T phi, T psi, T theta);
Point3D & rotate(const Point3D& centar, T phi, T psi, T theta);
static int print_cnt() {return cnt;}
private:
T data[3];
static int cnt;
};
#endif
Implementacija svih metoda dolazi u datoteku zaglavlja — uključujući i globalne operatore.
Napomena
|
Ako klasa nije parametrizirana sve metode koje nisu inline se implementiraju u izvornoj datoteci
(ne datoteci zaglavlja). Pri tome datoteka zaglavlja deklarira klasu ali i sve operatore definirane izvan
klase jer i oni pripadaju tipu koji klasa definira. |
Zadatak. Implementirati sve metode klase Point3D<T>
.
Operatori dohvata
Pod operatorima dohvata podrazumijevamo operatore *
(dereferenciranja) i ->
.
Napomena
|
Operator -> mora biti član klase. Operator dereferenciranja ne mora nužno biti član
klase, ali je obično prirodno definirati ga kao člana klase. |
Primjer preopterećenja operatora dohvata ilustrirat ćemo na klasi SmartBFC
.
class SmartBFC{
public:
SmartBFC(BFC *p) : ptr(p), cnt(new int(1)) {}
SmartBFC(const SmartBFC & orig): ptr(orig.ptr), cnt(orig.cnt) { ++*cnt; }
SmartBFC& operator=(const SmartBFC&);
~SmartBFC(){ if(--*cnt == 0) { delete ptr; delete cnt; } }
// Pomoćne funkcije
const BFC * get_ptr() const { return ptr; }
int use_count() const { return *cnt; }
private:
BFC * ptr;
int * cnt;
};
Preopterećivanjem operatora dohvata klasi SmartBFC
dajemo pokazivačku sintaksu.
Ona će na razini sintakse funkcionirati kao pokazivač i stoga se naziva pametni pokazivač
(smart pointer).
Operator dereferenciranja
-
Kada je član klase, ne uzima parametar i vraća referencu na tip. Stoga se može koristiti na lijevoj strani operatora pridruživanja. Uvijek definiramo konstantnu i nekonstantnu verziju operatora.
Operator strelica
Operator strelica također ne uzima parametar. Na desnoj se strani operatora
->
uvijek nalazi identifikator (ime funkcije ili varijable koju dohvaćamo), a ne
objekt nekog tipa. To čini ovaj operator posebnim. Kad god prevodilac naiđe na izraz oblika:
pt->metoda();
on generira kod na sljedeći način:
-
Ako je
pt
pokazivač na objekt koji ima metodumetoda()
, onda prevodilac generira kod za poziv metode; -
Ako je
pt
objekt tipa klase koja definira operator->
tada je poziv oblikapt->metoda()
isto što ipt.operator->()->metoda()
. To znači daoperator->()
mora vratiti:-
ili pokazivač na objekt klase koja deklarira funkciju
metoda()
-
ili objekt klase koja definira
operator->()
. U ovom drugom slučaju definicija je rekurzivna.
-
-
U svim ostalim slučajevima kod je pogrešan.
Operator strelicu i operator dereferenciranja preopterećujemo po konstantnosti kako bismo mogli dohvatiti konstantne i nekonstantne objekte.
Korigirana klasa SmartBFC
Klasa SmartBFC
s pokazivačkom semantikom:
class SmartBFC{
public:
SmartBFC(BFC *p) : ptr(p), cnt(new int(1)) {}
SmartBFC(const SmartBFC & orig): ptr(orig.ptr), cnt(orig.cnt) { ++*cnt; }
SmartBFC& operator=(const SmartBFC&);
~SmartBFC(){ if(--*cnt == 0) { delete ptr; delete cnt; } }
const BFC& operator*() const {return *ptr;}
BFC& operator*() {return *ptr;}
const BFC* operator->() const {return ptr;}
BFC* operator->() {return ptr;}
int use_count() const { return *cnt; }
private:
BFC * ptr;
int * cnt;
};
Primjer upotrebe
Sada SmartBFC
objekt koristimo kao da je pokazivač. Jedina dodatna metoda je use_count()
:
int main()
{
SmartBFC spt1(new BFC(13));
SmartBFC spt2 = spt1;
cout << (*spt1).get() << endl;
spt2->set(2);
cout << spt1->get() << endl;
const SmartBFC spt3(new BFC(11));
cout << (*spt3).get() << endl;
cout << spt3->get() << endl;
cout << spt1.use_count() << endl;
return 0;
}
Operatori inkrementiranja i dekrementiranja (++/--
)
Operatori inkrementiranja i dekrementiranja implementiraju se najčešće u klasama koje modeliraju iteratore i daju pokazivačku semaniku svojim objektima. Mi ćemo ih ilustrirati na jednoj jednostavnoj verziji iteratora kroz polje nekog tipa.
Polje
Imamo vlastiti spremnik i želimo za njega napisati iterator. Na primjer:
template <typename T>
class Polje
{
private:
T* data; // dinamički alociran blok memorije
size_t n_el;
public:
Polje(size_t n) : data(new T[n]), n_el(n) {}
~Polje(){ delete [] data; }
size_t size() const { return n_el; }
using iterator = IteratorKrozPolje<T>; // naš iterator
// ...
};
Konstrukcija iteratora
Želimo iterator direktnog dohvata koji će provjeravati da li je iterator u korektnim granicama te izbaciti izuzetak ako nije.
template <typename T>
class IteratorKrozPolje {
private:
T* begin;
T* end;
T* current;
public:
IteratorKrozPolje(T* b, size_t size) : begin(b), end(b+size),
current(b) {}
IteratorKrozPolje(T* b, size_t size, T* curr) : begin(b),
end(b+size), current(curr) {}
//....
};
Klasa je parametrizirana tipom T
koji predstavlja tip elementa polja.
Imamo pokazivač na početak polja (begin
), pokazivač na mjesto
iza zadnjeg elementa polja (end
) i pokazivač na tekući
element polja (current
).
Prefiks operatori: deklaracija
Ovi operatori kad su članovi klase ne uzimaju argumente i da bi bili konzistentni s ugrađenim verzijama trebaju vratiti referencu na objekt na kojem su pozvani. Imali bismo:
template <typename T>
class IteratorKrozPolje {
private:
T* begin;
T* end;
T* current;
public:
IteratorKrozPolje(T* b, size_t size);
IteratorKrozPolje(T* b, size_t size, T* curr);
IteratorKrozPolje& operator++(); // prefix
IteratorKrozPolje& operator--();
//....
};
Prefiks operatori: implementacija
U implementaciji provjeravamo da li povećanjem ili smanjenjem izlazimo iz zadanih granica, i ako je to slučaj izbacujemo out_of_range izuzetak koji je definiran u <stdexcept> zaglavlju.
template <typename T>
IteratorKrozPolje<T>& IteratorKrozPolje<T>::operator++() {
if(current == end)
throw std::out_of_range("Inkrementiranje end pokazivaca.");
++current;
return *this;
}
template <typename T>
IteratorKrozPolje<T>& IteratorKrozPolje<T>::operator--() {
if(current == begin)
throw std::out_of_range("Dekrementiranje pokazivaca na 1. element.");
--current;
return *this;
}
U implementaciji na pokazivaču current
koristimo standardne operatore dekrementiranja
i inkrementiranja.
Postfiks operatori: Deklaracija
Način razlikovanja prefiks i postfiks verzije ovih operatora posve je artificijelan.
Postfiks verzije uzimaju jedan dodatni parametar tipa int
koji služi samo
razlikovanju dviju verzija operatora. Prilikom korištenja postfiks forme
prevodilac sam predaje operatoru nulu kao argument. Nadalje, postfiks forma vraća objekt a ne referencu na objekt.
Sjetimo se da postfiks forma mijenja objekt, ali vraća kopiju nepromijenjenog objekta.
template <typename T>
class IteratorKrozPolje
{
private:
T* begin;
T* end;
T* current;
public:
//....
IteratorKrozPolje operator++(int); // postfix
IteratorKrozPolje operator--(int);
//....
};
Postfiks operatori: Implementacija
Napravimo kopiju objekta, inkrementiramo/dekrementiramo pokazivač i vraćamo kopiju:
template <typename T>
IteratorKrozPolje<T> IteratorKrozPolje<T>::operator++(int)
{
IteratorKrozPolje<T> tmp{*this}; // zapamti pokazivač
// može biti end!
++*this; // Pozovi prefix verziju
return tmp; // Vrati staru vrijednost. Sada tmp != end
}
template <typename T>
IteratorKrozPolje<T> IteratorKrozPolje<T>::operator--(int)
{
IteratorKrozPolje<T> tmp{*this}; // Kopija objekta
--*this; // Pozovi prefix verziju
return tmp; // Vrati staru vrijednost
}
Potpuna klasa
U IteratorKrozPolje
dodajemo još nekoliko konstruktora, operator dereferenciranja i operatore uspoređivanja.
template <typename T>
class IteratorKrozPolje
{
private:
T* begin;
T* end;
T* current;
public:
IteratorKrozPolje(T* b, size_t size);
IteratorKrozPolje(T* b, size_t size, T* curr);
IteratorKrozPolje& operator++(); // prefix
IteratorKrozPolje& operator--();
IteratorKrozPolje operator++(int); // postfix
IteratorKrozPolje operator--(int);
T& operator*() {
if(current == end) throw ...
return *current;
}
const T& operator*() const {
if(current == end) throw ...
return *current;
}
T* operator->() {
if(current == end) throw ...
return current;
}
const T* operator->() const {
if(current == end) throw ...
return current;
}
template <typename U>
friend bool operator==(const IteratorKrozPolje<U>& lhs,
const IteratorKrozPolje<U>& rhs);
template <typename U>
friend bool operator!=(const IteratorKrozPolje<U>& lhs,
const IteratorKrozPolje<U>& rhs);
// Ove tipove očekuje klasa std::iterator_traits
// koju koristi standardna biblioteka.
using difference_type = std::ptrdiff_t;
using value_type = T;
using pointer = T*;
using reference = T&;
using iterator_category = std::random_access_iterator_tag;
// Kategorije iteratora:
// input_iterator_tag, output_iterator_tag, forward_iterator_tag,
// bidirectional_iterator_tag,random_access_iterator_tag
};
Primjena na klasu Polje
Konstruirajmo klasu koja će koristiti naš iterator:
template <typename T>
class Polje
{
private:
T* data;
size_t n_el;
public:
Polje(size_t n) : data(new T[n]), n_el(n) {}
size_t size() const { return n_el; }
using iterator = IteratorKrozPolje<T>;
iterator begin() { return IteratorKrozPolje<T>(data, n_el); }
const iterator end()
{ return IteratorKrozPolje<T>(data, n_el, data+n_el); }
~Polje(){ delete [] data; }
};
Korištenjem simboličkog imena iterator
ime je iteratora skriveno tako da
Polje
možemo koristiti na standardan način, kao i druge spremnike
u standardnoj biblioteci.
int main()
{
Polje<double> v(10); // Polje od 10 elemenata
Polje<double>::iterator it = v.begin();
for(double x=0.0; it != v.end(); ++it, x += 5.0)
*it = x;
return 0;
}
Napomena: Prefiks i postfiks operatore ++
i --
možemo pozivati i eksplicitno.
double polje[] = {1.0,2.0,3.0,4.0,5.0};
IteratorKrozPolje<double> iter(polje, 5);
iter.operator++(0); // postfix operator++
iter.operator++(); // prefix operator++
Zadatak. Implementirajte konstantan iterator i cbegin()
i cend()
metode.
Funkcijski objekti
C++ nam dozvoljava da u klasi definiramo operator funkcijskog poziva
()
. Objekt te klase se ponaša kao funkcija pa se stoga
naziva funkcijski objekt.
Evo primjera klase koja implementira operator()
.
class CutOff{
private:
double min;
double max;
public:
CutOff(double low, double high) : min(low), max(high) {assert(min <= max);}
double operator()(double x);
};
Operator je implementiran na sljedeći način:
double CutOff::operator()(double x){
if(x <=min) return min;
else if(x > max) return max;
else return x;
}
Upotreba:
void f(CutOff fun, double a, double b, unsigned n)
{
for(unsigned i=0; i< n; ++i)
std::cout << fun(a + i*(b-a)/n) << std::endl;
}
Poziv
fun(a + i*(b-a)/n)
je ekvivalentna s
fun.operator()(a + i*(b-a)/n)
Parametrizirane funkcije i funkcijski objekti
Napravimo od funkcije f
iz prethodnog
primjera parametriziranu funkciju:
template <typename T>
void f(T fun, double a, double b, unsigned n)
{
for(unsigned i=0; i< n; ++i)
std::cout << fun(a + i*(b-a)/n) << std::endl;
}
Definirajmo još i običnu funkciju
double funkcija(double x) { return x; }
Sada će program
int main()
{
CutOff b(0,1);
std::cout << "Funkcijski objekt:" << std::endl;
f(b,-1.0,2.0,6);
std::cout << "Funkcija:" << std::endl;
f(funkcija,-1,2,6);
return 0;
}
ispisati
Funkcijski objekt:
0
0
0
0.5
1
1
Funkcija:
-1
-0.5
0
0.5
1
1.5
Parametrizirana funkcija je dakle prihvatila i funkcijski objekt i funkciju kao prvi argument. Prevodioc je generirao dvije (preopterećene) funkcije iz template-a:
void f(CutOff fun, double a, double b, unsigned n)
void f(double (*fun)(double), double a, double b, unsigned n)
Lambda izrazi
Kada prevodilac naiđe na lambda izraz on definira anonimnu klasu koja implementira funkcijski objekt. Na primjer, u kodu
int size = 3;
int n = std::count_if(vec.begin(), vec.end(),
[size](std::string const & s)
{ return s.size() > size; }
);
bit će definirana klasa oblika:
class ImeKojeGeneriraPrevodilac{
public:
ImeKojeGeneriraPrevodilac(int sz) : size(sz) {}
bool operator()(std::string const & s) const
{ return s.size() > size; }
private:
int size;
};
i gornji kod je ekvivalentan s
int n = std::count_if(vec.begin(), vec.end(), ImeKojeGeneriraPrevodilac(size));
-
Varijable koje lambda hvata su argumenti konstruktora.
-
Operator funkcijskog poziva je konstantan tako da lambda ne može mijenjati varijablu uhvaćenu po vrijednosti (za to mora biti definirana mutable).
Generički lambda izrazi
C++14 definira generičke lambda izraze u kojima se tipovi argumenata izraza deduciraju auto-dedukcijom. Na primjer,
int size = 3;
int n = std::count_if(vec.begin(), vec.end(),
[size](auto & s)
{ return s.size() > size; }
);
U ovom slučaju će prevodilac definirati klasu oblika:
class ImeKojeGeneriraPrevodilac{
public:
ImeKojeGeneriraPrevodilac(int sz) : size(sz) {}
template <typename T>
auto operator()(T & s) const { return s.size() > size; }
private:
int size;
};
i gornji kod je ekvivalentan s
int n = std::count_if(vec.begin(), vec.end(), ImeKojeGeneriraPrevodilac(size));
Operator funkcijskog poziva nije instanciran pri kreiranju lambde već u trenutku korištenja operatora.
Funkcijski objekti i std::function
Objekt koji možemo pozvati s danim parametrima može biti: funkcija, pokazivač na funkciju,
lambda izraz, funkcijski objekt i objekt kreiran pomoću std::bind
.
Svi ti objekti i kada imaju istu signaturu poziva imaju različite tipove i stoga ih, na primjer, ne možemo spremiti u isti spremnik. Primjer:
int add(int i, int j) { return i+j; } // funkcija
struct Div{ // klasa
int operator()(int i, int j) { return i/j; }
};
auto mod = [](int i, int j){ return i % j; }; // lambda
std::map<std::string, int(*)(int,int)> binops;
binops.insert({"+", add}); // o.k.
binops.insert({"%", mod}); // greška, ovisno o implementaciji
binops.insert({"/", Div()}); // greška
Ta problem riješava predložak std::function<T>
iz zaglavlja
<functional>
koji za dani tip poziva T
može biti inicijaliziran bilo kojim
objektom koji može biti pozvan na taj način. Primjer:
std::function<int(int,int)> f1 = add;
std::function<int(int,int)> f2 = mod;
std::function<int(int,int)> f3 = Div();
std::map<std::string, std::function<int(int,int)>> binops;
binops.insert({"+",f1});
binops.insert({"%",f2});
binops.insert({"/",f3});
std::cout << binops["+"](2,3) << std::endl;
std::cout << binops["%"](4,3) << std::endl;
std::cout << binops["/"](9,3) << std::endl;
Konverzije
-
Konverzija u tip klase
-
Konverzija iz tipa klase u …
Konverzija u tip klase
Vrši ih konstruktor koji uzima jedan argument. Ako nije definiran kao explicit
onda se konverzija vrši automatski.
class Racionalan{
friend ostream& operator<<(ostream&, const Racionalan&);
friend Racionalan operator+(const Racionalan&, const Racionalan&);
friend Racionalan operator-(const Racionalan&, const Racionalan&);
friend Racionalan operator*(const Racionalan&, const Racionalan&);
friend Racionalan operator/(const Racionalan&, const Racionalan&);
private:
int n; // Brojnik
int m; // Nazivnik
public:
Racionalan(int brojnik=0, int nazivnik = 1) : n(brojnik), m(nazivnik)
{assert(m != 0);}
Racionalan& operator+=(const Racionalan& rhs);
Racionalan& operator-=(const Racionalan& rhs);
};
Zadatak. Implementirajte sve operatore klase Racionalan
.
Sjedeći kod će ispravno funkcionirati:
Racionalan a(5,3), b(4,3);
cout << a + b << endl;
cout << a + 3 << endl;
cout << 1 + a << endl;
Problem:
cout << a + 3.14 << endl;
Naredba je posve legalna, ali daje isti rezultat kao i a + 3
.
Konverzija iz tipa klase u …
Konverziju iz tipa klase u neki drugi tip obavljaju operatori konverzije.
Operator konverzije u tip T
deklarira se na sljedeći način:
operator T() const;
Pogledajmo primjer operatora koji tip Racionalan
konvertira u double
.
class Racionalan{
friend ostream& operator<<(ostream&, const Racionalan&);
friend Racionalan operator+(const Racionalan&, const Racionalan&);
friend Racionalan operator-(const Racionalan&, const Racionalan&);
friend Racionalan operator*(const Racionalan&, const Racionalan&);
friend Racionalan operator/(const Racionalan&, const Racionalan&);
private:
int n; // Brojnik
int m; // Nazivnik
public:
explicit
Racionalan(int brojnik=0, int nazivnik = 1);
operator double() const { return static_cast<double>(n)/m;}
Racionalan& operator+=(const Racionalan& rhs);
Racionalan& operator-=(const Racionalan& rhs);
};
Sintaksa operatora konverzije:
-
Tip u koji se vrši konverzija dio je imena operatora i dolazi iz ključne riječi
operator
. Taj tip može biti primitivan, tip klase ili ime uvedeno stypedef
. Pri tome, kako operator konverzije vraća vrijednost tipa u koji se konverzija vrši, nije moguće definirati operator konverzije u tip koji ne može biti povratna vrijednost funkcije — kao što je polje ili funkcija. Ostali složeni tipovi (pokazivači i reference) su dozvoljeni. -
Operator konverzije mora biti član klase. On ne uzima parametre i ne deklarira povratnu vrijednost jer je ona dana imenom operatora.
-
Prirodno je operator konverzije deklarirati kao konstantan operator.
Sada će se naredba
cout << a + 3.14 << endl;
ispravno izvršiti i dati rezultat a + 3.14 = 4.80667
umjesto
a + 3.14 = 14/3
kojeg smo dobivali ranije. Ali, uočimo dobro
da smo naš konstruktor sada deklarirali eksplicitnim. Pogledajmo što be se
desilo pri naredbi a + 3.14
da to nismo učinili: Prevodilac bi javio
sljedeću grešku:
error: ambiguous overload for operator+ in a + 3.14000000000000012434497875801753252744674682617e+0
note: candidates are: operator+(double, double) <built-in>
note: Racionalan operator+(const Racionalan&, const Racionalan&)
Prevodilac je zaključio da su ugrađeni operator zbrajanja i operator definiran u klasi Racionalan
jednako dobri kandidati i poziv je stoga dvosmislen. Naime, sada postoje dvije mogućnosti
koje su jednakovaljane:
-
Ili konvertirati
3.14
pomoću konstruktora u tipRacionalan
i zatim primijeniti operator zbrajanja iz klaseRacionalan
; -
Ili konvertirati
a
pomoću operatora konverzije u tipdouble
i iskoristiti primitivan operator zbrajanja.
Budući da se u oba slučaja na jednom parametru mora izvršiti konverzija definiranu korisnikom, a među takvim konverzijama prevodilac ne radi razlike, poziv je dvosmislen.
Konverzije se obavljaju automatski
Konverzije koje obavljaju operatori konverzije i one koje obavlja konstruktor pozivaju se automatski u svim situacijama koje dovode od implicitnih konverzija. To su sljedeće situacije:
-
U izrazima u kojima sudjeluju operandi različitih tipova;
-
U uvjetima
if
iwhile
naredbe (konverzija u tipbool
); -
Pri predaji parametara funkciji i vraćanju vrijednosti iz funkcije u pozivni program;
-
Pri predaji parametara preopterećenim operatorima;
-
Kod eksplicitnog korištenja konverzije.
Eksplicitna konverzija
cout << static_cast<double>(c) << endl;
je ekvivalentna funkcijskom pozivu
cout << c.operator double() << endl;
Problemi s konverzijama
Kada implementiramo konverziju iz tipa klase u neki drugi tip moramo osigurati da postoji samo jedan način na koji se ta konverzija vrši.
To pravilo je moguće narušiti na dva načina:
-
Dvije klase dozvoljavaju istu konverziju;
-
Postoji više konverzija u tipove međusobno vezane konverzijama.
Dvije klase s istom konverzijom
struct B;
struct A{
A() = default;
A(B const &); // konverzija iz B u A
};
struct B{
operator A() const; // konverzija iz B u A
};
A f(A const & a){
// ...
}
B b;
A a = f(b); // Dvosmislen poziv. b može biti konvertiran
// u A na dva načina.
A a = f(b.operator A()); // prva mogućnost
A a = f(A(b)); // druga mogućnost
Više konverzija u tipove međusobno vezane konverzijama
Te situacije vode do dvosmislenih poziva.
struct C{
C(int); // konverzija iz int u C
C(double); // konverzija iz double u C
operator int() const; // konverzija iz C u int
operator double() const;// konverzija iz C u double
// ..
};
void g(long double x){
// ...
}
C c(1);
g(c); // c -> double -> long double ili
// c -> int -> long double
long int x = 1l;
C cc(x); // long int -> double ili long int -> int ?
Explicitan operator konverzije
Standard C++2011 dozvoljava explicit
deklaraciju operatora konverzije.
Eksplicitan operator konverzije ne sudjeluje u implicitnim konverzijama već se može
pozvati jedino eksplicitno (odnosno kroz static_cast
).
To pravilo ima jednu iznimku: operator konverzije u tip bool
i kada je definiran kao explicit
vrši implicitnu konverziju u
ovim situacijama:
-
U uvjetu
if
,while
,do
ifor
naredbe; -
U uvjetu operatora
?:
. -
Kao operand logičkih operatora.
struct A{
explicit operator bool() const;
// ...
};
A a;
bool isDone = a; // greška, implicitne konverzije nisu dozvoljene
if( a ) { // o.k.
// ...
}
Primjer: operator >>
iz STL-a definira eksplicitan operator konverzije u bool
i
zato možemo koristiti izraze oblika:
while(std::cin >> value)
Konverzije i preopterećenje funkcija
Standardne konverzije mogu biti kombinirane s korisničkim konverzijama, ali vrijedi:
Napomena
|
U implicitnim i eksplicitnim konverzijama prevodilac dozvoljava najviše jednu korisničku konverziju (konverziju definiranu unutar klase operatorom konverzije ili konstruktorom). |
Selekcija funkcije iz skupa preopterećenih funkcija dešava se u tri koraka:
-
Određivanje skupa kandidata;
-
Nalaženje dobrih kandidata unutar skupa svih kandidata;
-
Određivanje najboljeg kandidata.
Skup kandidata su sve funkcije istog imena vidljive u dosegu poziva, odnosno u prvom okružujućem dosegu u kome se nalazi bar jedan kandidat. Operatori se tretiraju kao funkcije, ali ovdje postoji mala iznimka. Operatori članovi klase tretiraju se ravnopravno s operatorima definiranim izvan klase.
Dobri kandidati čine podskup kandidata koji se mogu pozvati sa zadanim parametrima. U određivanju dobrih kandidata određuju se i konverzije koje su potrebne za poziv svake pojedine funkcije ili operatora.
Najbolji kandidat ne smije biti lošiji od drugih niti po jednom parametru, a po jednom mora biti bolji od svih. Pri tome se konverzije rangiraju kako bi se ostvario poredak na skupu konverzija. Najboljeg kandidata nema kada ima više "jednako dobrih" i tada je poziv dvosmislen.
-
U svakom nizu konverzija može biti samo jedna korisnička. Ako dva niza konverzija koriste istu korisničku konverziju, onda se cjelokupna konverzija rangira prema rangu standardnih konverzija koje se vrše prije ili poslije korisničke konverzije.
-
Ako dva niza konverzija koriste različitu korisničku konverziju, onda se ti nizovi smatraju jednakovrijednim bez obzira na rang standardnih konverzija koje se vrše prije ili poslije korisničke konverzije. Takve situacije lako dovode do dvosmislenosti poziva.
Kada se desi dvosmisleni poziv zbog implicitnih konverzija ponekad se situacija može razriješiti eksplicitnim pozivom operatoru konverzije ili konstruktoru, no to nije uvijek slučaj.