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