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

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

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;
}

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:

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

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 metodu metoda(), onda prevodilac generira kod za poziv metode;
  • Ako je 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:
    • 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));

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

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:

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:

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:

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 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:

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:

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.