wtorek, 25 marca 2014

Przeciążanie operatorów w C++

Liczba argumentów znajdujących się na liście argumentów przeciążanego operatora zależy od dwóch czynników:
  • czy jest to operator jednoargumentowy czy dwuargumentowy,
  • czy operator jest zdefiniowany jako funkcja globalna (brak wskaźnika "this") czy też jako funkcja składowa:
    • operator jednoargumentowy: jeden argument dla funkcji globalnej i brak argumentu dla funkcji składowej,
    • operator dwuargumentowy: dwa argumenty dla funkcji globalnej i jeden argument dla funkcji składowej.
Przykładowy kod demonstrujący przeciążanie operatorów jednoargumentowych:
#include <iostream>

using namespace std;

// Definicja klasy dla przypadku gdy funkcje operatorowe nie są funkcjami składowymi (dekleracje przyjaźni z funkcjami globalnymi).

class integer {
    int variable;

    integer * that() {
        return this;
    }

public:
    // Konstruktory.

    integer(int variable_constructor = 0) : variable(variable_constructor) { }

    // Funkcje.

    void show_variable() {
        cout << "variable ma wartość = " << variable << endl;
    }

    // Deklaracje przyjaźni (funkcje operatorowe globalne).

    friend const integer & operator+(const integer & argument);
    friend const integer operator-(const integer & argument);
    friend const integer operator~(const integer & argument);
    friend integer * operator~(integer & argument);
    friend int operator!(const integer & argument);
    friend const integer & operator++(integer & argument); // Przedrostkowy.
    friend const integer operator++(integer & argument, int); // Przyrostkowy.
    friend const integer & operator--(integer & argument);
    friend const integer operator--(integer & argument, int);
};

// Globalne funkcje operatorowe.

// Poniższe definicje jako argumenty przyjmują referencje co nie powoduje skutków ubocznych.

const integer & operator+(const integer & argument) {
    return argument; // Jednoargumentowy operator w tym wypadku nic nie zmienia.
}

const integer operator-(const integer & argument) {
    return (-argument.variable);
}

const integer operator~(const integer & argument) {
    return integer(~argument.variable);
}

integer * operator~(integer & argument) {
    return argument.that(); // "& argument" jest rekurencyjne.
}

int operator!(const integer & argument) {
    return !argument.variable;
}

// Teraz definicje funkcji, których argumenty nie są referencjami do stałych.

// Operator przedrostkowy (zwraca wartość po inkrementacji). Kompilator generuje wywołanie funkcji w postaci "operator++(zmienna)".

const integer & operator++(integer & argument) {
    argument.variable++;

    return argument;
}

// Operator przyrostkowy (zwraca wartość przed inkrementacją). Kompilator generuje wywołanie funkcji "operator++(zmienna, int)".

const integer operator++(integer & argument, int) {
    integer before(argument.variable);

    argument.variable++;

    return before;
}

const integer & operator--(integer & argument) { // Przedroskowy.
    argument.variable--;

    return argument;
}

const integer operator--(integer & argument, int) { // Przyrostkowy.
    integer before(argument.variable);

    argument.variable--;

    return before;
}

// Definicja klasy dla przypadku gdy funkcje operatorowe są funkcjami składowymi (niejawny argument "this").

class byte {
    int variable;

public:
    // Konstruktory.

    byte(int variable_constructor = 0) : variable(variable_constructor) { }

    // Stałe funkcje składowe (brak skutków ubocznych).

    const byte & operator+() const {
        return * this;
    }

    const byte operator-() const {
        return byte(-variable);
    }

    const byte operator~() const {
        return byte(~variable);
    }

    byte operator!() const {
        return byte(!variable);
    }

    byte * operator&() {
        return this;
    }

    // Niestałe funkcje składowe (występują skutki uboczne).

    const byte & operator++(); // Przedrostkowo.
    const byte operator++(int); // Przyrostkowo.
    const byte & operator--(); // Przedrostkowo.
    const byte operator--(int); // Przyrostkowo.
};

const byte & byte::operator++() { // Kompilator generuje wywołanie funkcji w postaci "byte::operator++()".
    variable++;

    return * this;
}

const byte byte::operator++(int) { // Kompilator generuje wywołanie funkcji w postaci "byte::operator++(int)".
    byte before(variable);

    variable++;

    return before;
}

const byte & byte::operator--() {
    variable--;

    return * this;
}

const byte byte::operator--(int) {
    byte before(variable);

    variable--;

    return before;
}

int main() {
}

Przykładowy kod prezentujący przeciążanie operatorów dwuargumentowych:
#include <iostream>

using namespace std;

// Definicja klasy dla przypadku gdy funkcje operatorowe nie są funkcjami składowymi (dekleracje przyjaźni z funkcjami globalnymi).

class integer {
    int variable;

public:
    // Konstruktory.

    integer(int variable_constructor = 0) : variable(variable_constructor) { }

    // Deklaracje przyjaźni.

    friend const integer operator+(const integer & left, const integer & right);
    friend const integer operator-(const integer & left, const integer & right);
    friend const integer operator*(const integer & left, const integer & right);
    friend const integer operator/(const integer & left, const integer & right);
    friend const integer operator%(const integer & left, const integer & right);
    friend const integer operator^(const integer & left, const integer & right);
    friend const integer operator&(const integer & left, const integer & right);
    friend const integer operator|(const integer & left, const integer & right);
    friend const integer operator<<(const integer & left, const integer & right);
    friend const integer operator>>(const integer & left, const integer & right);
    friend integer & operator+=(integer & left, const integer & right);
    friend integer & operator-=(integer & left, const integer & right);
    friend integer & operator*=(integer & left, const integer & right);
    friend integer & operator/=(integer & left, const integer & right);
    friend integer & operator%=(integer & left, const integer & right);
    friend integer & operator^=(integer & left, const integer & right);
    friend integer & operator&=(integer & left, const integer & right);
    friend integer & operator|=(integer & left, const integer & right);
    friend integer & operator>>=(integer & left, const integer & right);
    friend integer & operator<<=(integer & left, const integer & right);
    friend int operator==(const integer & left, const integer & right);
    friend int operator!=(const integer & left, const integer & right);
    friend int operator<(const integer & left, const integer & right);
    friend int operator>(const integer & left, const integer & right);
    friend int operator<=(const integer & left, const integer & right);
    friend int operator>=(const integer & left, const integer & right);
    friend int operator&&(const integer & left, const integer & right);
    friend int operator||(const integer & left, const integer & right);
};

// Definicja klasy dla przypadku gdy funkcje operatorowe są funkcjami składowymi (niejawny argument "this").

class byte {
    int variable;

public:
    // Konstruktory.

    byte(int variable_constructor = 0) : variable(variable_constructor) { }

    // Stałe funkcje składowe (brak skutków ubocznych).

    const byte operator+(const byte & right) const {
        return byte(variable + right.variable);
    }

    const byte operator-(const byte & right) const {
        return byte(variable - right.variable);
    }

    const byte operator*(const byte & right) const {
        return byte(variable * right.variable);
    }

    const byte operator/(const byte right) const {
        return byte(variable / right.variable);
    }

    const byte operator%(const byte & right) const {
        return byte(variable % right.variable);
    }

    const byte operator^(const byte & right) const {
        return byte(variable ^ right.variable);
    }

    const byte operator&(const byte & right) const {
        return byte(variable & right.variable);
    }

    const byte operator|(const byte & right) const {
        return byte(variable | right.variable);
    }

    const byte operator<<(const byte & right) const {
        return byte(variable << right.variable);
    }

    const byte operator>>(const byte & right) const {
        return byte(variable >> right.variable);
    }

    // Przypisania modyfikujące i zwracające l-wartość.

    byte & operator=(const byte & right); // Funkcja operator= może być tylko składowa.
    byte & operator+=(const byte & right);
    byte & operator-=(const byte & right);
    byte & operator*=(const byte & right);
    byte & operator/=(const byte & right);
    byte & operator%=(const byte & right);
    byte & operator^=(const byte & right);
    byte & operator&=(const byte & right);
    byte & operator|=(const byte & right);
    byte & operator>>=(const byte & right);
    byte & operator<<=(const byte & right);

    // Operatory warunkowe (zwracające true lub false).

    int operator==(const byte & right) const {
        return (variable == right.variable);
    }

    int operator!=(const byte & right) const {
        return (variable != right.variable);
    }

    int operator<(const byte & right) const {
        return (variable < right.variable);
    }

    int operator>(const byte & right) const {
        return (variable > right.variable);
    }

    int operator<=(const byte & right) const {
        return (variable <= right.variable);
    }

    int operator>=(const byte & right) const {
        return (variable == right.variable);
    }

    int operator&&(const byte & right) const {
        return (variable && right.variable);
    }

    int operator||(const byte & right) const {
        return (variable || right.variable);
    }
};

// Implementacja przeciążonych operatorów dla klasy integer.

const integer operator+(const integer & left, const integer & right) {
    return integer(left.variable + right.variable);
}

const integer operator-(const integer & left, const integer & right) {
    return integer(left.variable - right.variable);
}

const integer operator*(const integer & left, const integer & right) {
    return integer(left.variable * right.variable);
}

const integer operator/(const integer & left, const integer & right) {
    return integer(left.variable / right.variable);
}

const integer operator%(const integer & left, const integer & right) {
    return integer(left.variable % right.variable);
}

const integer operator^(const integer & left, const integer & right) {
    return integer(left.variable ^ right.variable);
}

const integer operator&(const integer & left, const integer & right) {
    return integer(left.variable & right.variable);
}

const integer operator|(const integer & left, const integer & right) {
    return integer(left.variable | right.variable);
}

const integer operator<<(const integer & left, const integer & right) {
    return integer(left.variable << right.variable);
}

const integer operator>>(const integer & left, const integer & right) {
    return integer(left.variable >> right.variable);
}

// Przypisania modyfikujące i zwracające l-wartość (dla klasy integer).

integer & operator+=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable += right.variable;
    }

    return left;
}

integer & operator-=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable -= right.variable;
    }

    return left;
}

integer & operator*=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable *= right.variable;
    }

    return left;
}

integer & operator/=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable /= right.variable;
    }

    return left;
}

integer & operator%=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable %= right.variable;
    }

    return left;
}

integer & operator^=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable ^= right.variable;
    }

    return left;
}

integer & operator&=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable &= right.variable;
    }

    return left;
}

integer & operator|=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable |= right.variable;
    }

    return left;
}

integer & operator>>=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable >>= right.variable;
    }

    return left;
}

integer & operator<<=(integer & left, const integer & right) {
    if (& left == & right) { // Przypisanie do samego siebie.
        left.variable <<= right.variable;
    }

    return left;
}

// Operatory warunkowe zwracające true lub false (dla klasy integer).

int operator==(const integer & left, const integer & right) {
    return left.variable == right.variable;
}

int operator!=(const integer & left, const integer & right) {
    return left.variable != right.variable;
}

int operator<(const integer & left, const integer & right) {
    return left.variable < right.variable;
}

int operator>(const integer & left, const integer & right) {
    return left.variable > right.variable;
}

int operator<=(const integer & left, const integer & right) {
    return left.variable <= right.variable;
}

int operator>=(const integer & left, const integer & right) {
    return left.variable >= right.variable;
}

int operator&&(const integer & left, const integer & right) {
    return left.variable && right.variable;
}

int operator||(const integer & left, const integer & right) {
    return left.variable || right.variable;
}

// Funkcje składowe klasy byte.

byte & byte::operator=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable = right.variable;
    }

    return * this;
}

byte & byte::operator+=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable += right.variable;
    }

    return * this;
}

byte & byte::operator-=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable -= right.variable;
    }

    return * this;
}

byte & byte::operator*=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable *= right.variable;
    }

    return * this;
}

byte & byte::operator/=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable /= right.variable;
    }

    return * this;
}

byte & byte::operator%=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable /= right.variable;
    }

    return * this;
}

byte & byte::operator^=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable ^= right.variable;
    }

    return * this;
}

byte & byte::operator&=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable &= right.variable;
    }

    return * this;
}

byte & byte::operator|=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable |= right.variable;
    }

    return * this;
}

byte & byte::operator>>=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable >>= right.variable;
    }

    return * this;
}

byte & byte::operator<<=(const byte & right) {
    if (this == & right) { // Przypisanie do siebie samego.
        variable <<= right.variable;
    }

    return * this;
}

int main() {
}

Przykład przeciążania operatora przecinkowego:
class after {
public:
    const after & operator,(const after &) const {
        return * this;
    }
};

class before { };

before & operator,(int, before & argument) {
    return argument;
}

Przykład przeciążania operatora wyłuskania wskaźnika do składowej:
#include <iostream>

using std::cout;

using std::endl;

class object {
public:
    typedef int (object::*pmf)(int) const; // "operator->*" musi zwrócić obiekt posiadający "operator()".

    class function_object {
        object * class_object_pointer;
        pmf pointer;

    public:
        // Zapamiętuje wskaźnik obiektu number wskaźnik składowej.

        function_object(object * object_pointer_constructor, pmf pointer_constructor) : class_object_pointer(object_pointer_constructor), pointer(pointer_constructor) {
            cout << "Kontruktor function_object.\n";
        }

        // Wywołanie wykorzystujące wskaźnik obiektu number wskaźnik składowej.

        int operator()(int number) const {
            cout << "Wywolanie funkcji skladowej \"operator()\" klasy function_object klasy object.\n";

            return (class_object_pointer->*pointer)(number); // Wywołanie.
        }
    };

    function_object operator->*(pmf pmf) {
        cout << "Wywolanie \"operator->*\" klasy object." << endl;

        return function_object(this, pmf);
    }

    int action_1(int number) const;
    int action_2(int number) const;
    int action_3(int number) const;
};

int object::action_1(int number) const {
    cout << "Czynnosc nr 1 obiektu.\n";

    return number;
}

int object::action_2(int number) const {
    cout << "Czynnosc nr 2 obiektu.\n";

    return number;
}

int object::action_3(int number) const {
    cout << "Czynnosc nr 3 obiektu.\n";

    return number;
}

int main() {
    object variable;
    object::pmf pmf = &object::action_1;

    cout << "Zwraca " << (variable->*pmf)(1) << "." << endl;

    cout << endl;

    pmf = &object::action_3;

    cout << "Zwraca " << (variable->*pmf)(2) << "." << endl;

    cout << endl;

    pmf = &object::action_2;

    cout << "Zwraca " << (variable->*pmf)(3) << "." << endl;
}

Kod ukazujący przykładowe przeciążenia operatora indeksu tablicy:
#include <iostream>

using std::cout;

using std::endl;

template <typename type, size_t elements> class table {
    type series[elements];
    size_t elements_number;

public:
    table() : elements_number(elements) { }

    type & operator[](size_t elements) {
        return series[elements];
    }

    const type & operator[](size_t elements) const {
        return series[elements];
    }
};

int main() {
    const size_t elements = 5;
    table <short, elements> series;

    for (int i = 0; i < elements; i++) {
        series[i] = i;

        cout << series[i] << endl;
    }
}

Operatory, których nie można przeciążać:
  • operator wyboru składowej (jako "obiekt.składowa"),
  • operator wyłuskania wskaźnika do składowej w postaci ",*",
  • brak operatora potęgowania,
  • nie można samodzielnie tworzyć nowych operatorów,
  • nie można zmieniać reguł dotyczących priorytetów operatorów.
Na ogół jeżeli nie ma to znaczenia to przeciążone operatory powinny być funkcjami składowymi (podkreśla to związek między operatorem i jego klasą). Czasem zachodzi jednak potrzeba aby argument znajdujący się po lewej stronie operatora był obiektem jakiejś innej klasy. Typowym tego przykładem jest przeciążanie operatorów "<<" i ">>" dla operacji związanych ze strumieniami wejścia i wyjścia:
#include <iostream>
#include <sstream>
#include <cstring>
#include <ostream>
#include <istream>

using std::cout;

using std::endl;

using std::ostream;

using std::istream;

using std::stringstream;

class table {
    enum { elements_number = 5 };
    int series[elements_number];

public:
    table() {
        memset(series, 0, (elements_number * sizeof(*series)));
    }

    int & operator[](int index) {
        return series[index];
    }

    friend ostream & operator<<(ostream & stream, const table & argument);
    friend istream & operator>>(istream & stream, table & argument);
};

ostream & operator<<(ostream & stream, const table & argument) {
    for (int iterator = 0; iterator < argument.elements_number; iterator++) {
        stream << argument.series[iterator];

        if (iterator != argument.elements_number - 1)
            stream << ", ";
    }

    stream << endl;

    return stream;
}

istream & operator>>(istream & stream, table & argument) {
    for (int iterator = 0; iterator < argument.elements_number; iterator++)
        stream >> argument.series[iterator];

    return stream;
}

int main() {
    stringstream input("1 2 3 45 88");
    table one;

    input >> one;

    cout << one;
}

Przykład przeciążania operatorów new i delete w obrębie klasy:
#include <iostream>

using std::cout;

using std::cerr;

using std::endl;

using std::bad_alloc;

class object {
    enum { size = 10 };
    char table_char[size]; // Niewykorzystywana (zajmuje tylko pamięć).
    static unsigned char pool[];
    static bool alloc_map[];

public:
    enum { objects_number = 100 }; // Liczba dopuszczalnych obiektów.
    void * operator new (size_t) throw(bad_alloc);
    void operator delete(void *);
};

unsigned char object::pool[objects_number * sizeof(object)];
bool object::alloc_map[objects_number] = { false };

int main() {
    object * one[object::objects_number];

    try {
        for (int iterator = 0; iterator < object::objects_number; iterator++) {
            one[iterator] = new object;
        }

        new object; // Brak pamięci.
    }
    catch (bad_alloc) {
        cerr << "Brak pamieci.\n";
    }

    delete one[10];

    one[10] = 0;

    object * two = new object; // Użycie zwolnionej pamięci.

    delete two;

    for (int iterator = 0; iterator < object::objects_number; iterator++)
        delete one[iterator];
}

// Wielkość jest ignorowana. Zakłada się, zę to obiekt klasy object.

void * object::operator new(size_t) throw(bad_alloc){
    for (int iterator = 0; iterator < objects_number; iterator++) {
        if (!alloc_map[iterator]) {
            cout << "Uzywany blok: " << iterator << ".\n";

            alloc_map[iterator] = true; // Oznaczenie bloku jako używanego.

            return (pool + (iterator * sizeof(object)));
        }
    }

    cerr << "Brak pamieci.\n";

    throw bad_alloc();
}

void object::operator delete(void * memory) {
    if (!memory) { // Sprawdź czy wskaźnik nie jest pusty.
        return;
    }

    // Zakładamy, że obiekt został utworzony w dostępnej puli.

    // Wyznaczanie numeru przydzielonego bloku.

    unsigned long block = (unsigned long)memory - (unsigned long)pool;

    block /= sizeof(object);

    cout << "Zwalnianie bloku: " << block << ".\n";

    alloc_map[block] = false;
}

Przykład przeciążania new i delete w ujęciu globalnym:
#include <cstdlib>
#include <cstdio>

void * operator new(size_t length){
    void * memory = malloc(length);

    if (!memory)
        printf("Brak pamieci."); // Podczas tworzenia obiektu klasy iostream (np. globalne cin, cout oraz cerr) jest wywoływany operator new przydzielający mu pamięć. Użycie funkcji printf nie grozi blokadą systemu gdyż ta funkcja nie wywołuje operatora new podczas swojej inicjalizacji.

    return memory;
}

void operator delete(void * memory) {
    free(memory);
}

Proponuje się następujące zalecenia co do przeciążania operatorów:
  • operatory jednoargumentowe powinny być funkcjami składowymi,
  • funkcje "operator=", "operator()", "operator[]", "operator->" oraz "operator->*" muszą być funkcjami składowymi,
  • funkcje "operator+=",  "operator-=",  "operator/=",  "operator*=",  "operator^= ",  "operator&=, "operator|=",  "operator%=",  "operator>>=" oraz  "operator<<=" powinny być funkcjami składowymi,
  • wszystkie pozostałe operatory dwuargumentowe nie powinny być funkcjami składowymi. 
Źródła:
- Eckel B., Thinking in C++. Edycja polska, Helion SA, 2002,
- Grębosz J., Symfonia C++, EDITION 2000, 2008.