4. 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 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 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ř. 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é 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 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?

courses/b2b99ppc/tutorials/04.txt · Last modified: 2024/02/26 13:50 by nentvond