====== Przeciążanie operatorów ====== =====Wykorzystanie przeciążania operatorów===== Pisząc program w C%%++%% można korzystać zarówno z typów wbudowanych jak i z typów zdefiniowanych przez siebie (jako nowe klasy). Wszystkie typy wbudowane mają zdefiniowane operatory porównania i przypisania, inkrementacji czy dekrementacji; tablice, wskaźniki czy referencje również posiadają zestaw operatorów ułatwiających (czy nieraz wręcz umożliwiających korzystanie z ich własności). Dla typów zdefiniowanych przez programistę można zdefiniować (przeciążyć) operatory istniejące w języku C%%++%% dla danego typu, dzięki czemu stosowanie ich w kontekście tych typów ma sens. Jednym z typowych przykładów przeciążania operatorów jest przeładowanie operatora wstawiania i pobierania danych ze strumienia:// Deklaracja znajduje się w pliku Point.h #ifndef POINT_H #define POINT_H #include class Point { public: ... void SetX(double x); void SetY(double y); //opcjonalna definicja pozwalająca na uzyskanie //dostępu do prywatnych pól z wewnątrz definicji //funkcji zadeklarowanej poniżej // friend std::istream& operator>>(std::istream &, Point&); ... private: double x_, y_; }; //właściwa deklaracja, przeciążająca //operator >> dla strumienia wejściowego //i klasy punkt std::istream& operator>>(std::istream &is, Point& point); #endif I definicja w pliku Point.cpp //Definicja w Point.cpp #include "Point.h" #include #include using ::std::istream; using ::std::ws; //Helper functions: void CheckNextChar(char c, istream* is) { int next_char = is->peek(); if (next_char != c) { throw runtime_error("invalid character"); } is->ignore(); } void IgnoreWhitespace(istream* is) { (*is) >> ws; } double ReadNumber(istream* is) { double d; (*is) >> d; return d; } // Właściwa definicja, obydwa argumenty funkcji nie //są zadeklarowane jako const, bo obydwa są modyfikowane //wewnątrz funkcji (STL nie używa naszej konwencji z przekazywaniem //przez wskaźnik) istream& operator>>(istream & input, Point& p){ CheckNextChar('(', &input); p.SetX(ReadNumber(&input)); CheckNextChar(',', &input); IgnoreWhitespace(&input); p.SetY(ReadNumber(&input)); CheckNextChar(')', &input); return input; // Umożliwia cin >> a >> b >> c; } Powyższy przykład umożliwia wczytanie punktu podanego w formacie //(12, 22)//. Należy zwrócić uwagę, że operator ">>" nie może być metodą klasy a funkcją zaprzyjaźnioną jeśli chcemy wywołać go jako cin >> punkt; Jeśli operator zadeklarowany byłby jako metoda klasy trzeba by go wywoływać na przykład tak:punkt >> cin ===== Jak to działa ===== Operatory to "zwyczajne" funkcje w języku C%%++%%. Kompilator zamienia każde pojawienie się operatora na wywołanie odpowiedniej funkcji. Zakładając że operatory + oraz * są składowymi klas, tak będzie wyglądało użycie tych operatorów dla kompilatora: Matrix a, b; a + b; // Dla kompilatora oznacza to a.operator+(b); (argument a zostanie przesłany jako this do funkcji) a * b; // dla kompilatora oznacza to a.operator*(b); (j.w.) Można także zdefiniować niektóre operatory jako funkcje nieskładowe (jak w przykładzie z poprzedniej sekcji). Zakłądjąc że operatory + i * są zadeklarowane jako funkcje zaprzyjaźnione, tak będzie wyglądało użycie tych operatorów dla kompilatora: Matrix a, b; a + b; // Dla kompilatora oznacza to operator+(a,b); a * b; // dla kompilatora oznacza to operator*(a,b); cin >> a; // dla kompilatora oznacza to operator>>(cin, a) cin >> a >> b; // dla kompilatora oznacza to operator>>( operator>>(cin, a), b) //stąd też wynika, że wartość zwracana przez funkcję operator>> musi być taka sama jak pierwszy argument tej funkcji ===== Operatory jedno- i dwuargumentowe===== Operatory **jednoargumentowe** można przeciążyć za pomocą niestatycznej funkcji składowej nie pobierającej żadnych argumentów (jeden niejawny argument jest przekazywany jako **this**) lub za pomocą funkcji nie będącej składową klasy, pobierającą jeden argument, który jest obiektem (przekazywanie przez wartość) lub referencją do obiektu tej klasy. **Wyjątkiem** jest operator rzutowania, który NIE może być zadeklarowany jako funkcja nieskładowa. Funkcje nieskładowe mogą być dodatkowo deklarowane jako zaprzyjaźnione z klasą dla której są zdefiniowane wtedy mają dostęp do prywatnych składników klasy. Załóżmy, że klasa Text jest naszą wersją klasy //string//, przechowującej napisy i umożliwiającą manipulowanie nimi. Chcąc zdefiniować na przykład operator **!** (logiczne zaprzeczenie) dla klasy Napis, który będzie zwracał //true// jeśli obiekt będzie zawierał pusty napis, a w każdym innym przypadku //true//, możemy to zrobić na da sposoby: class Text{ public: // składowa klasy bool operator!() const; // LUB (opocjonalnie) funkcja nieskładowa zaprzyjaźniona // friend bool operator!()(const Text&) ... }; //LUB funkcja nieskładowa, która dodatkowo może być zaprzyjaźniona //bool operator!()(const Text &text) Operatory **dwuargumentowe** można przeciążyć za pomocą niestatycznej funkcji składowej klasy pobierającej jeden argument (pierwszy argument jest przekazywany niejawnie jako **this**) lub za pomocą funkcji nie będącej składową klasy, pobierającą dwa argumenty z których jeden jest obiektem lub referencją do obiektu tej klasy. **Wyjątkami** są operatory przypisania, **[]** oraz **->**, które NIE mogą być zadeklarowane jako funkcje zaprzyjaźnione. class Text{ public: // składowa klasy (pomysl dlaczego zwracamy const) const Text operator+=(const Text& txt); // LUB funkcja zaprzyjaźniona friend const Text operator+=()(Text& lhs, const Text& rhs); ... }; const Text operator+=()(Text& lhs, const Text& rhs); =====Konwersja typów===== Specyficznym rodzajem operatorów są tzw. operatory rzutowania. Wykorzystywane one są do konwersji typów, czy inaczej mówiąc do rzutowania. W prosty sposób można rzutować pomiędzy typami wbudowanymi: double number = 12.34; int integer_value = static_cast(number); Aby możliwe było rzutowanie pomiędzy typami zdefiniowanymi przez programistę, konieczna jest definicja operatorów realizujących taką funkcjonalność. Operatory rzutowania NIE mogą być zadeklarowane jako funkcje zaprzyjaźnione - muszą być składowymi klasy. Dodatkowo niczego nie zwracają: //Complex.h class Complex{ public: operator Matrix() const; ... }; //Complex.cpp Complex::operator Matrix() const { return Matrix {{*this}}; } Dzięki powyższej deklaracji i odpowiedniej definicji możliwe będzie następujące wywołanie: Complex c("4i5"); //Powinien utworzyć macierz 1x1 z wartością 4i5 w jedynej komórce Matrix m = (Matrix)c; ===== Operatory inkrementacji i dekrementacji===== Operatory **%%++%%** i **- -** mogą również być przeciążane. Mogą one być jednak stosowane zarówno w wersji prefiksowej i postfiksowej. ==== Operator prefiksowy==== Chcąc przeładować operator inkrementacji dla klasy //Complex// tak aby można było wykonać następującą operację: Complex c(1); ++c; //kompilator tak anprawdę wywołuje c.operator++() należy zadeklarować operator **%%++%%** w następujcy sposób: class Complex{ public: Complex &operator++(); }; ==== Operator postfiksowy==== Aby możliwe było wywołanie inkrementacji w notacji postfiksowej: Complex c(1); c++; //kompilator tak naprawdę wywołuje c.operator++(0) konieczne jest zadeklarowanie operatora **%%++%%** w następujący sposób: class Complex{ public: // parametr "int" jest "dowolnym argumentem" który jest wykorzystywany // przez kompilator do odróżnienia operatora postfiksowego od // operatora prefiksowego. Podczas wywołania c++ // kompilator wywoła tak naprawdę c.operator++(0) Complex operator(int); }; =====Tabela operatorów===== ^Operatory, które można przeciążać^^^^^^^ |+|-|*|/|%|^|&| |||~|!|=|<|>|+=| |-=|*=|/=|%=|^=|&=||=| |<<|>>|>>=|<<=|==|!=|<=| |>=|&&||||%%++%%|--|->*|,| |->|[]|()|new|delete|new[]|delete[]| ^Operatory, których nie można przeciążać^^^^^ |.|.*|::|?:|sizeof| ===== Czy przeciążyć operator ===== Zasadniczo jeśli zastosowanie operatora w kodzie używającym danego obiektu (wywołującym metody) jest czytelne, jednoznaczne i intuicyjne wtedy można rozważyć przeciążenie operatora dla danego typu. W przeciwnym wypadku nie należy na siłę deklarować kodu operatorów, gdyż pogorszą one tylko jakość i czytelność kodu. ===== Dodatkowy rozszerzony opis ===== Zapoznać się z opisem na stronie [[http://en.cppreference.com/w/cpp/language/operators|Operatory]]. Na tej stronie są dodatkowe przykłady. ======Ćwiczenia====== - [1 plus] Przetestować operator wczytywania dla klasy Point i dopisać operator wypisania ("<<"). Sprawdzić w jakich przypadkach ten operator się wywołuje. (Przetestować zarówno na cout, cin, jak i stringstream). - Napisać klasę Student z polami id, first_name, last_name, program, year, gdzie wszystkie pola poza year są typu string, a pole year ma własny typ StudyYear. - [1 plus] zdefiniować operator %%++%% i %%--%% pre(in|de)krementacji dla StudyYear - [2 plus] zdefiniować operator << i >> zapisu i odczytu ze strumienia, przy następujących formatach: * StudyYear - 2 * Student - Student {id: "2030001234", first_name: "Arkadiusz", last_name: "Kowalski", program: "informatyka", year: 2} * StudentRepository - [Student {id: "203000001",... , Student {id: "...] - [1 plus] zdefiniować operator == porównywania dla StudyYear, Student i StudentRepository i < dla StudyYear - [1 plus] zdefiniować operator '[]' zakresu pozwalający na pobranie z repozytorium studenta o określonym id - [1 plus] co należy zrobić z powyższym operatorem, żeby była możliwa operacja repository["201500022324"].ChangeFirstName("Ziemowit"); i została zmienione imię studenta już w repozytorium. - [1 plus] dopisz operator rzutowania dla StudyYear do typu int. - [2 plusy] Zdefiniować klasę Zipper ze statyczną metodą zip(std::vector, std::vector), która pozwoli na uruchomienie następującego kodu: int foo(const vector &v1, const vector &v2) { for (const pair &p : Zipper::zip(v1,v2)) { if (p.first == "elo") { return p.second+4; } } return 0; } Podpowiedź: udostępniona też [[http://coliru.stacked-crooked.com/a/ff24fd32a47a6e18|TU]] #include #include #include #include using std::cout; using std::endl; using std::vector; using std::string; using std::pair; using std::literals::string_literals::operator""s; class ZipperIterator { public: pair operator*() const; //wmagane w linii 74 ZipperIterator &operator++(); //wymagane w linii 73 for(_;_;TU) bool operator!=(const ZipperIterator &other) const; //wymagane w linii 73 for(_;TU;_) private: //TODO }; //umożliwia przeglądanie dwóch wektorów na raz, w jednej pętli range-for class Zipper { public: static Zipper zip(const vector &vs, const vector &vi); ZipperIterator begin(); //wymagane w linii 73 for(TU;_;_) ZipperIterator end(); //wymagane w linii 73 for(_;TU;_) private: //TODO }; int main() { vector vi = {1,2,3}; vector vs = {"one"s,"two"s,"three"s}; //PROSTY przykład for (const auto i : vi) { cout << i; } cout << endl; //to samo bez auto: for (const int i : vi) { cout << i; } cout << endl; //przetłumaczony powyższa petla mniej wiecej tak jak to kompilator //rozumie: for (vector::iterator it = vi.begin(); it != vi.end(); ++it) { const int i = *it; cout << i; } cout << endl; //Przykład z zipperem for (const auto &p : Zipper::zip(vs,vi)) { cout << p.second << " is " << p.first; } cout << endl; //to samo bez auto: for (const pair &p : Zipper::zip(vs,vi)) { cout << p.second << " is " << p.first; } cout << endl; //przetłumaczony powyższa petla mniej wiecej tak jak to kompilator //rozumie: Zipper tmp = Zipper::zip(vs,vi); for (ZipperIterator it = tmp.begin(); it != tmp.end(); ++it) { const pair &p = *it; cout << p.second << " is " << p.first; } cout << endl; } - **[5 punktów] Napisz klasę WordCounter (podobne ćwiczenie było już tylko jako struktura z języka C, tym razem ma być to pełna klasa z C%%++%%), która będzie zawierać licznik słów. Należy zdefiniować klasę Word, która będzie stanowiła klucz (słowo zliczane) i klasę Counts, która będzie przechowywała liczbę zliczeń. Zarówno klasa Word i Counts powinna zawierać pojedynczy typ prymitywny. Klasa WordCounter powinna mieć konstruktor domyślny inicjalizujący pusty słownik i konstruktor z listą inicjalizacyjną pozwalający zliczyć podane słowa. Dodatkowo należy zdefiniować statyczną funkcję FromInputStream przyjmującą jako parametr istream pokazujący na tekst. W funkcji FromInputStream powinno nastąpić odczytanie tekstu i zbudowanie indeksu wyrazów, tak aby każdy wyraz z pliku miał odpowiadający sobie obiekt std::pair w dowolnym kontenerze c%%++%%. Pole typu //string// wewnątrz obiektów Word powinno odpowiadać danemu słowu, natomiast pole typu //integer// wewnątrz Counts ilości powtórzeń tego słowa w wewnętrznym słowniku. Zawartość strumienia jest __dowolnym tekstem!__ Analizując go, ignorujemy znaki interpunkcyjne, spacje tabulatory, etc. i wczytujemy tylko słowa. ** * Przeładuj operator klasy WordCounter **[]** tak aby możliwe było poniższe wywołanie:std::ifstream is ("myfile.txt"); WordCounter wc = WordCounter.FromInputStream(&is); // w zmiennej ilość powinna znaleźć się ilość powtórzeń // słowa "programowanie" w pliku "myfile.txt" int ilosc = wc["programowanie"]; * Przeładuj operator **%%<<%%** dla klasy WordCounter, aby możliwe było wyświetlenie raportu o ilości słów i ich liczebności w danym pliku. Dane wyświetlane powinny być posortowane malejąco. Do tego celu wykorzystaj metodę //[[http://en.cppreference.com/w/cpp/algorithm/sort|sort]]// z biblioteki //algorithm// - wykorzystaj dowolny kontener z biblioteki standardowej! (w dokumentacji w przykładzie na dole jest przykład jak użyć dowolnego warunku porównującego w konteście sortowania, zerknąć na przykład środkowy z użyciem struktury customLess). * Przeładuj operatory porównania (//<,>,==//) dla klasy Counts (porównywanie względem liczebności). * Przeładuj operator **%%++%%** dla klasy Counts, tak aby można było szybko inkrementować liczebność danego słowa podczas budowania licznika. * Zdefiniuj funkcje DistinctWords zwaracającą ilość różnych słów w liczniku * Zdefiniuj funkcje TotalWords zwaracającą ilość słów w liczniku z uwzględnieniem ich liczności (DistinctWords <= TotalWords) * Zdefiniuj funkcje Words zwarającą zbiór wszystkich słów w liczniku