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