MySQL Java JavaScript PHP Python HTML-CSS C-sharp C++ Go

Supraincărcarea operatorilor

Supraincărcarea operatorilor (operator overloading) permite definirea pentru obiectele claselor a operatorilor încorporați, precum +, -, * etc. Pentru a defini un operator pentru obiectele propriei clase, este necesar să se definească o funcție al cărei nume conține cuvântul operator și simbolul operatorului suprascris. Funcția operatorului poate fi definită fie ca membru al clasei, fie în afara clasei.

Se pot suprascrie doar acei operatori care sunt deja definiți în C++. Nu pot fi creați operatori noi. De asemenea, nu se poate modifica numărul de operanzi, asociativitatea sau prioritatea acestora.

Dacă funcția operatorului este definită ca o funcție separată și nu este membru al clasei, atunci numărul de parametri al acestei funcții coincide cu numărul de operanzi ai operatorului. De exemplu, o funcție care reprezintă un operator unar va avea un parametru, iar o funcție care reprezintă un operator binar - doi parametri. Dacă operatorul are doi operanzi, atunci primul operand este transmis primului parametru al funcției, iar al doilea operand - celui de-al doilea parametru. În același timp, cel puțin unul dintre parametri trebuie să reprezinte tipul clasei.

Definirea formală a operatorilor ca funcții-membru ale clasei:

// operator binar
ReturnType operator Op(Type right_operand);

// operator unar
ClassType& operator Op();

Definirea formală a operatorilor sub formă de funcții care nu sunt membri ai clasei:

// operator binar
ReturnType operator Op(const ClassType& left_operand, Type right_operand);
// definiție alternativă, unde clasa pentru care se creează operatorul este operandul din dreapta
ReturnType operator Op(Type left_operand, const ClassType& right_operand);
// operator unar
ClassType& operator Op(ClassType& obj);

Aici ClassType reprezintă tipul pentru care este definit operatorul. Type este tipul celuilalt operand, care poate coincide sau nu cu primul. ReturnType este tipul rezultatului returnat, care de asemenea poate coincide cu unul dintre tipurile operanzilor sau poate fi diferit. Op reprezintă operatorul propriu-zis.

Să analizăm un exemplu cu clasa Counter, care stochează un număr:

#include <iostream>
  
class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    Counter operator + (const Counter& counter) const
    {
        return Counter{value + counter.value};
    }
private:
    int value;
};
 
int main()
{
    Counter c1{20};
    Counter c2{10};
    Counter c3 = c1 + c2;
    c3.print();   // Value: 30
}

Aici, în clasa Counter este definit operatorul de adunare, al cărui scop este să adune două obiecte de tip Counter:

Counter operator + (const Counter& counter) const
{
    return Counter{value + counter.value};
}

Obiectul curent va reprezenta operandul din stânga al operației. Obiectul transmis funcției prin parametrul counter va reprezenta operandul din dreapta. Aici parametrul funcției este definit ca referință constantă, dar nu este obligatoriu. De asemenea, funcția operatorului este marcată ca const, dar nici asta nu este obligatoriu.

Rezultatul operatorului de adunare este un nou obiect Counter, în care valoarea value este egală cu suma valorilor value ale ambilor operanzi.

După definirea operatorului, putem aduna două obiecte de tip Counter:

Counter c1{20};
Counter c2{10};
Counter c3 {c1 + c2};
c3.print();   // Value: 30

În mod similar, putem defini funcția operatorului în afara clasei:

#include <iostream>
  
class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    int value;  // funcția externă a operatorului nu poate accesa membrii privați
};

// definim operatorul de adunare în afara clasei
Counter operator + (const Counter& c1, const Counter& c2) 
{
    return Counter{c1.value + c2.value};
}
  
int main()
{
    Counter c1{20};
    Counter c2{10};
    Counter c3 {c1 + c2};
    c3.print();   // Value: 30
}

Dacă operatorul binar este definit ca funcție externă, ca în acest caz, atunci el primește doi parametri. Primul parametru va reprezenta operandul din stânga al operației, iar al doilea parametru — operandul din dreapta.

Însă, în comparație cu codul precedent, aici s-au făcut câteva modificări suplimentare. În primul rând, o funcție externă, desigur, nu poate accesa membrii privați ai clasei, așa că pentru a accesa acești membri va trebui să creăm funcții speciale care returnează valorile membrilor. Eu, pentru simplitate, am făcut ca variabila value să fie publică. O altă soluție în acest caz ar fi putut fi definirea funcției operatorului ca funcție prietenă (friend).

Al doilea aspect este că funcțiile externe ale operatorilor nu pot fi constante. De aceea, definirea operatorilor în interiorul clasei are anumite avantaje.

Merită menționat că nu este obligatoriu să returnăm un obiect al clasei. Se poate returna orice alt tip de obiect, în funcție de situație. De asemenea, putem defini funcții de operatori suprasolicitați suplimentare.

#include <iostream>
  
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    Counter operator + (const Counter& counter) const
    {
        return Counter{value + counter.value};
    }
    int operator + (int number) const
    {
        return value + number;
    }
private:
    int value;
};
 
  
int main()
{
    Counter counter{20};
    int number = counter + 30;
    std::cout << number << std::endl;   // 50
}

Aici este definită a doua versiune a operatorului de adunare, care adună obiectul Counter cu un număr și returnează tot un număr. De aceea, operandul din stânga al operației trebuie să fie de tipul Counter, iar operandul din dreapta - de tip int.

Ce operatori unde trebuie suprascrişi? Operatorii de atribuire, indexare ([]), apelare (()), acces la membru prin pointer (->) trebuie definiţi ca funcţii-membru ale clasei. Operatorii care modifică starea obiectului sau sunt direct legaţi de obiect (increment, decrement), de obicei se definesc tot ca funcţii-membru ale clasei. Operatorii de alocare şi dealocare a memoriei (new new[] delete delete[]) se definesc doar ca funcţii care nu sunt membri ai clasei. Toţi ceilalţi operatori pot fi definiţi ca funcţii separate, şi nu ca membri ai clasei.

Operatorii de comparaţie

Rezultatul operatorilor de comparaţie (==, !=, <, >), de regulă, este o valoare de tip bool. De exemplu, vom suprascrie aceşti operatori pentru tipul Counter:

#include <iostream>
  
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    bool operator == (const Counter& counter) const
    {
        return value == counter.value;
    }
    bool operator != (const Counter& counter) const
    {
        return value != counter.value;
    }
    bool operator > (const Counter& counter) const
    {
        return value > counter.value;
    }
    bool operator < (const Counter& counter) const
    {
        return value < counter.value;
    }
private:
    int value;
};
 
  
int main()
{
    Counter c1(20);
    Counter c2(10);
    bool b1 = c1 == c2;     // false
    bool b2 = c1 > c2;   // true
  
    std::cout << "c1 == c2 = " << std::boolalpha << b1 << std::endl;    // c1 == c2 = false
    std::cout << "c1 > c2 = " << std::boolalpha << b2 << std::endl;     // c1 > c2 = true
}

Dacă este vorba despre o comparație simplă pe baza câmpurilor clasei, atunci pentru operatorii == și != este mai simplu să folosim operatorul special default:

#include <iostream>
  
class Counter
{
public:
    Counter(int val)
    {
        value =val;
    }
    void print() 
    {
        std::cout << "Value: " << value << std::endl;
    }
    bool operator == (const Counter& counter) const = default;
    bool operator != (const Counter& counter) const = default;
private:
    int value;
};
 
  
int main()
{
    Counter c1(20);
    Counter c2(10);
    bool b1 = c1 == c2;     // false
    bool b2 = c1 != c2;       // true
  
    std::cout << "c1 == c2 = " << std::boolalpha << b1 << std::endl;    // c1 == c2 = false
    std::cout << "c1 != c2 = " << std::boolalpha << b2 << std::endl;     // c1 != c2 = true
}

De exemplu, în cazul operatorului ==:

bool operator == (const Counter& counter) const = default;

În mod implicit vor fi comparate toate câmpurile clasei, pentru care este definit operatorul ==. Dacă valorile tuturor câmpurilor sunt egale, atunci operatorul va returna true.

Operatorii de atribuire

Operatorul de atribuire, de obicei, returnează o referință către operandul din stânga al expresiei:

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    void print()
    {
        std::cout << "Value: " << value << std::endl;
    }
    // operatorul de atribuire
    Counter& operator += (const Counter& counter)
    {
        value += counter.value;
        return *this;   // returnăm referința la obiectul curent
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2{50};
    c1 += c2;
    c1.print();     // Value: 70
}

Operațiile unare

Operațiile unare returnează de obicei un nou obiect creat pe baza celui existent. De exemplu, să luăm operația minus unar:

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    void print()
    {
        std::cout << "Value: " << value << std::endl;
    }
    // operatorul minus unar
    Counter operator - () const
    {
        return Counter{-value};
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2 = -c1;   // aplicăm operatorul minus unar
    c2.print();         // Value: -20
}

Aici operația minus unar returnează un nou obiect Counter, a cărui valoare value este egală cu valoarea obiectului curent înmulțită cu -1.

Operațiile de incrementare și decrementare

O dificultate aparte o reprezintă suprascrierea operatorilor de incrementare și decrementare, deoarece trebuie să definim atât forma prefixată, cât și forma postfixată a acestor operatori. Vom defini astfel de operatori pentru tipul Counter:

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    void print()
    {
        std::cout << "Value: " << value << std::endl;
    }
    // operatori prefixați
    Counter& operator++ ()
    {
        value += 1;
        return *this;
    }
    Counter& operator-- ()
    {
        value -= 1;
        return *this;
    }
    // operatori postfixați
    Counter operator++ (int)
    {
        Counter copy{*this};
        ++(*this);
        return copy;
    }
    Counter operator-- (int)
    {
        Counter copy{*this};
        --(*this);
        return copy;
    }
private:
    int value;
};

int main()
{
    Counter c1{20};
    Counter c2 = c1++;
    c2.print();       // Value: 20
    c1.print();       // Value: 21
    --c1;
    c1.print();       // Value: 20
}

Operatorii prefixați trebuie să returneze o referință la obiectul curent, care poate fi obținută cu ajutorul pointerului this:

Counter& operator++ ()
{
    value += 1;
    return *this;
}

În interiorul funcției putem defini logica de incrementare a valorii. În acest caz, valoarea value este mărită cu 1.

Operatorii postfixați trebuie să returneze valoarea obiectului înainte de incrementare, adică starea anterioară a obiectului. De aceea, forma postfixată returnează o copie a obiectului înainte de incrementare:

Counter operator++ (int)
{
    Counter copy{*this};
    ++(*this);
    return copy;
}

Pentru ca forma postfixată să se distingă de cea prefixată, versiunile postfixate primesc un parametru suplimentar de tip int, care nu este folosit. Deși, în principiu, l-am putea folosi.

Suprascrierea operatorului <<

Operatorul << acceptă doi parametri: o referință la un obiect de flux (operandul din stânga) și o valoare care trebuie afișată (operandul din dreapta). Apoi, el returnează o referință la același flux, ceea ce permite apeluri în lanț ale operatorului <<.

#include <iostream>

class Counter
{
public:
    Counter(int val)
    {
        value = val;
    }
    int getValue() const { return value; }
private:
    int value;
};

std::ostream& operator<<(std::ostream& stream, const Counter& counter)
{
    stream << "Value: ";
    stream << counter.getValue();
    return stream;
}

int main()
{
    Counter counter1{20};
    Counter counter2{50};
    std::cout << counter1 << std::endl;     // Value: 20
    std::cout << counter2 << std::endl;     // Value: 50
}

Fluxul standard de ieșire cout are tipul std::ostream. Prin urmare, primul parametru (operandul din stânga) reprezintă un obiect de tip ostream, iar al doilea (operandul din dreapta) — un obiect de tip Counter. Deoarece nu putem modifica definiția clasei std::ostream, trebuie să definim operatorul ca o funcție externă clasei Counter.

std::ostream& operator<<(std::ostream& stream, const Counter& counter)
{
    stream << "Value: ";
    stream << counter.getValue();
    return stream;
}

În acest caz, afișăm valoarea membrului value. Pentru a obține această valoare din afara clasei, am adăugat metoda getValue().

Valoarea returnată trebuie întotdeauna să fie o referință la același flux primit ca parametru (pentru a permite apeluri în lanț).

Exprimarea unui operator prin altul

Uneori este mai eficient să exprimăm un operator prin intermediul altuia, în loc să duplicăm logica. De exemplu:

#include <iostream>

class Counter
{
public:
    Counter(int n)
    {
        value = n;
    }
    void print() const
    {
        std::cout << "value: " << value << std::endl;
    }

    Counter& operator+=(const Counter& counter)
    {
        value += counter.value;
        return *this;
    }

    Counter operator+(const Counter& counter)
    {
        Counter copy{ value };
        copy += counter;
        return copy;
    }
private:
    int value;
};

int main()
{
    Counter counter1{20};
    Counter counter2{10};

    counter1 += counter2;
    counter1.print();   // value: 30

    Counter counter3{counter1 + counter2};
    counter3.print();   // value: 40
}

Mai întâi definim operatorul +=:

Counter& operator+=(const Counter& counter)
{
    value += counter.value;
    return *this;
}

Apoi, în operatorul +, copiem obiectul curent și aplicăm asupra copiei operatorul +=:

Counter operator+(const Counter& counter)
{
    Counter copy{ value };
    copy += counter;
    return copy;
}

Astfel, evităm duplicarea logicii și centralizăm implementarea într-o singură funcție.