====== 5. Přetížené operátory ====== ===== Třída Vektor ===== Cílem tohoto cvičení je implementovat třídu s podobnou funkcionalitou, jako má sekvenční kontejner [[https://en.cppreference.com/w/cpp/container/vector|std::vector]]. Třída bude obsahovat dynamicky alokované pole, které se bude podle potřeb realokovat a poskytne rozhraní v podobě přetížených operátorů pro přístup k jednotlivým prvkům struktury. ==== Základní verze ==== Základní verze třídy bude obsahovat dva konstruktory (neparametrický pro vytvoření vektoru nulové délky a konstruktor s jedním parametrem pro rezervaci paměti pro data), privátní metodu ''resize()'' pro změnu velikosti vektoru, konstantní veřejnou metedu ''size()'' pro zjištění aktuální velikost vektoru, veřejnou metodu ''push_back()'' pro vkládání dat na konec vektoru a přetížený operátor ''[]''. Konstruktory a metoda ''size()'' jsou definovány přímo v deklaraci třídy. class Vektor { int * pole; size_t velikost; void resize (size_t x); public: Vektor () : velikost(0) {pole = new int[1];} Vektor (size_t x) : velikost(x) {pole = new int[x];} size_t size () const {return velikost;} void push_back (int x); int operator[] (int x); } === Změna velikosti vektoru === Pro změnu velikosti pole včetně překopírování dat je zde použita strategie vytvoření nového dynamicky alokovaného pole, do kterého je překopírován obsah pomocí metody [[https://en.cppreference.com/w/cpp/algorithm/copy|std::copy]], která umožňuje kromě iterátorů pracovat i s běžnými iterátory a aktualizace atributu třídy na nový ukazatel. Všimněte si: * nejsou použity ''C'' funkce jako ''realloc'' nebo ''memcpy''; nepoužívejte je, nemíchejte ''C'' a ''C++'' kód * před aktualizací atributu ''pole'' je třeba dealokovat původně alokovanou paměť; to může způsobit problém v případě, že bychom se pokusili dealokovat pole nulové délky - tato operace není definována, proto je v neparametrickém konstruktoru alokováno pole velikosti ''1''. void Vektor::resize (size_t x) { int* tmp = new int [x]; std::copy(pole, pole+velikost, tmp); delete [] pole; pole = tmp; } === Vkládání dat na konec === Pro vkládání dat na konec vektoru slouží metoda ''push_back()''. void push_back (int x) { resize(velikost+1); pole[velikost++] = x; } === Přístup k prvkům vektoru === Pro čtení již zadaných dat vytvoříme přetížený operátor indexace, který vrací položku pole. int operator[] (int x) { return pole[x]; } === Hlavní program === V hlavním programu vyzkoušíme vložení 10 čísel typu ''int'' do vektoru a vypsání jejich hodnot. int main() { Vektor a; for (int i = 0; i < 10; i++) a.push_back (i); for (int i = 0; i < a.size(); i++) std::cout << a[i] << " "; std::cout << std::endl; return 0; } ==== Možná vylepšení ==== Hotová třída má funkcionalitu, kterou jsme si stanovili zadáním. To ovšem neznamená, že ji nemůžeme trochu vylepšit :-) ---- === Změna prvků vektoru === Po změnu prvků vektoru lze pochopitelně implementovat dedikovanou veřejnou metodu. Velmi jednoduchou úpravou přetíženého operátoru indexace lze ale dosáhnout stejného efektu: pokud totiž bude vracet operátor místo hodnoty referenci, bude možné použít operátor na levé straně výrazu (reference je L-hodnota). int & operator[] (int x) { return pole[x]; } Hlavní funkce ''main()'' pak může vypadat třeba takto: int main() { Vektor b(10); for (int i = 0; i < b.size(); i++) b[i] = i; for (int i = 0; i < b.size(); i++) std::cout << b[i] << " "; std::cout << std::endl; return 0; } === Vlastní iterátor === Určitě by bylo pěkné mít možnost využít pro naši třídu vlastní iterátor a s ním spojený komfort v podobě ''range for'' přístupu ke kontejnerům a podobně. Už víme, že iterátor je vlastně variantou ukazatele, musí ovšem respektovat jednotné rozhraní. Řekněme, že náme následující kód, ve kterém chceme procházet náš kontejner ''Vektor'': Vektor a(10); for (auto i : a) { std::cout << i; } Jak následující kód funguje? Překlač nejprve inicializuje rozsah (range), ve kterém bude kontejner procházet. K tomu potřebuje funkce ''begin()'' a ''end()'', které budou vhodným mechanismem vracet ukazatel na první a poslední prvek kontejneru. Takové metody by bylo celkem jednoduché implementovat v rámci třídy ''Vektor''. Ovšem nestačí to: z předchozího cvičení víme, že * pro posun iterátoru se používá operace inkrementace ''++'', návratovou hodnotou je reference na instanci třídy, obsahující inkrementovaný ukazatel; * konec iterace se vyhodnotí porovnáním postupně inkrementovaného iterátoru s iterátorem inicializovaným na konec kontejneru operátorem ''!='', návratovovou hodnotou je instance logického datového typu; * pro získání hodnoty odkazované iterátorem je třeba mít k dispozici operátor dereference ''*'', jehož návratovou hodnotou je reference interního datového typu, v tomto případě ''int''. To znamená přetížení operátorů, ovšem níkoliv ve třídě ''Vektor'', ale v nové třídě, jejíž instance bude inicializována ukazatelem na interní strukturu třídy ''Vektor'' a přetížené operátory nové třídy budou zajišťovat potřebnou funkcionalitu. class Iterator { int * ptr; public: Iterator (int * x) : ptr(x) {} int& operator*() const { return *ptr; } Iterator& operator++() { ptr++; return *this; } friend bool operator!= (const Iterator& a, const Iterator& b) { return a.ptr != b.ptr; }; }; V třídě ''Vektor'' pak implementujeme potřebné funkce: class Vektor { //... public: //... Iterator begin() {return Iterator(&pole[0]); Iterator end() {return Iterator(&pole[velikost]); }; Ve hlavním programu pak můžeme vyzkoušet průchod kontejneru pomocí range for: // kontejner a je uz naplnen cisly od 0 do 9 for (auto i : a) std::cout << i << " "; std::cout << std::endl; ==== Generické funkce ==== Kontejnery s iterátory mohou být argumenty některých generických funkcí, jako je např. [[https://en.cppreference.com/w/cpp/algorithm/accumulate|std::accumulate]]. Následující kód spočítá sumu prvků instance třídy ''Vektor'' a vypíše ji na standardní výstup: std::cout << std::accumulate (a.begin(), a.begin(), 0) << std::endl; Nepovinným čtvrtým argumentem je ukazatel na funkci, které [[https://en.cppreference.com/w/cpp/algorithm/accumulate|std::accumulate]] předává v každé iteraci dvě hodnoty: //akumulovanou hodnotu// a aktuální prvek získaný iterátorem. Pro následující příklad vytvoříme funkci ''multiply(int, int)'', která zpracuje hodnoty předáné volající funkcí a vynásobí je mezi sebou: int multiply (int a, int b) { return a * b; } // ... std::cout << std::accumulate (a.begin(), a.begin(), 1, multiply) << std::endl; V generických funkcích se mohou dobře uplatnit lambda funkce. V následujícím příkladu funkce [[https://en.cppreference.com/w/cpp/algorithm/accumulate|std::accumulate]] vypočítá aritmetický průměr prvků v kontejneru: auto lambda = [&](double x, double y) {return x + y / v.size();} std::cout << std::accumulate (a.begin(), a.end(), 0.0, lambda) << std::endl; Jak to funguje?