Vect operator+(const Vect&, const Vect&);
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:
:: |
.* |
. |
?: |
**
).
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. |
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.
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=
), indeksiranja (operator[]
),
funkcijski poziv (operator()
) i strelica (operator->
)
moraju biti metode članice (zahtjev prevodioca).
Konzistentnost s IO bibliotekom zahtjeva da operator <<
ostream&
kao prvi 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)
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];
// ...
};
Konzistentnost s IO bibliotekom zahtijeva da operator >>
istream&
kao prvi 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;
}
p
u slučaju greške.
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ž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;
}
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:
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;
}
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;
}
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>
.
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 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:
pt
pokazivač na objekt koji ima metodu metoda()
,
onda prevodilac generira kod za poziv metode;
pt
objekt tipa klase koja definira operator ->
tada je poziv oblika pt->metoda()
isto što i
pt.operator->()->metoda()
.
To znači da operator->()
mora vratiti:
metoda()
operator->()
.
U ovom drugom slučaju definicija je rekurzivna.
Operator strelicu i operator dereferenciranja preopterećujemo po konstantnosti kako bismo mogli dohvatiti konstantne i nekonstantne objekte.
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;
};
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 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.
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
// ...
};
Ž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
).
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--();
//....
};
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.
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);
//....
};
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
}
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
};
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.
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)
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)
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));
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.
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;
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;
cout << a + 3.14 << endl;
Naredba je posve legalna, ali daje isti rezultat kao i a + 3
.
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);
};
operator
. Taj tip može biti primitivan, tip klase ili ime uvedeno s
typedef
. 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.
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:
3.14
pomoću konstruktora u tip Racionalan
i zatim primijeniti
operator zbrajanja iz klase Racionalan
;
a
pomoću operatora konverzije u tip double
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 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:
if
i while
naredbe (konverzija u tip bool
);
Eksplicitna konverzija
cout << static_cast<double>(c) << endl;
je ekvivalentna funkcijskom pozivu
cout << c.operator double() << endl;
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:
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
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 ?
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:
if
, while
, do
i for
naredbe;
?:
.
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)
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:
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.
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.