7. hét: template, függvények kirajzolása

Czirkos Zoltán, Dobra Gábor · 2019.02.27.

Heti kiegészítő feladatok

Adott a lentebb látható Page osztály. Ez egy dinamikusan megadható méretű rajztáblát ad, amelyen karakterek jeleníthetőek meg:

Page p1(75, 20);
p1.set(50, 10, 'X');
p1.set(52, 11, 'Y');
p1.print();

Kiindulás: page.cpp letöltése

A Page osztály átdolgozása

Ez az osztály kétdimenziós dinamikus tömböt tartalmaz, ugyanakkor a másoló konstruktora, destruktora és értékadó operátora nincs megírva. Végezd el az alábbi módosításokat!

2. Egy dimenzió

A dinamikus 2D tömb használata fölösleges. Bár a rajztábla kívülről 2D tömbnek látszik, de belül nem kell annak lennie. Hogy sokkal egyszerűbb és gyorsabb legyen a memóriakezelés, érdemes belül 1D sorfolytonos tömböt használni. Pl. egy 3×4-es rajztábla eredetileg így épül fel:

1D sorfolytonos leképezéssel:

Ilyenkor a sorfolytonos tömb 3×4 = 12 elemű. Egy adott cella elérése az y * szélesség + x képlettel történik; a sor száma × sor szélesség taggal ugrunk az adott sorhoz, aztán a másik taggal azon belül egy cellához.

Alakítsd át ilyenre az osztályt, és teszteld a meglévő kóddal! A másoló konstruktort és destruktort még mindig ne írd meg!

3. Dinamikus tömb segédosztály

Vedd észre, hogy ezzel az átalakítással a Page osztály belsejében lényegében újraimplementáltál egy dinamikus tömb osztályt. A Page osztálynak jelenleg két feladata van:

  • rajztáblát valósít meg (set, clear, print),
  • és belül dinamikus memóriával pepecsel.

Ez így nincs rendjén, a dinamikus tömböt egyszer már megírtuk. Refaktoráld ezért a kódot! Írj egy nagyon-nagyon egyszerű CharTomb osztályt, ezzel a funkcionalitással:

CharArray arr(100);     // méret megadása konstruktorban
arr[12] = 'X';          // indexelő operátor

Egyelőre ennek se írd meg a másoló konstruktorát, értékadó operátorát. Helyette inkább dolgozd át a Page osztályodat, töröld ki belőle a memóriakezelést, használd fel adattagként az új CharTomb osztályod:

class Page {
  private:
    int w, h;
    CharArray array; 
};

Nagyon fontos: vedd észre, hogy ezzel a Page destruktora és másoló konstruktora fölöslegessé vált. Ha a CharArray destruktora és másoló konstruktora jó, akkor a Page-hez a fordító által generált függvények automatikusan jók lesznek!

4. Page std::vector-ral

Végül töröld ki a dummy CharArray osztályodat is. Van ilyen beépítve a C++-ba, a neve std::vector. Így kell használni:

#include <vector>

std::vector<int> v1(100);       // 100 elem
v1.resize(200);                 // átméretezés 200-ra
v1[12] = 123;

std::vector<char> v2(10, 'Q');  // 10 elem, csupa Q betű
v2.resize(30, 'Y');             // 30-ra nyúlik, Y-okkal tölti fel
std::cout << v2.size();

Írd át a Page osztályt úgy, hogy belül egy std::vector<char> legyen! Innentől kezdve new, delete, semmi hasonló nem lesz sehol.

5. Generikus Page

Módosítsd a Page osztályt, hogy sablonparamétere legyen a tárolt "egység" (ami eddig a karakter volt)!

Page<char> p1(75, 20);
p1.set(50, 10, 'X');
p1.set(52, 11, 'Y');
p1.print();

Egyelőre elég, ha a plot() függvény fixen Page<char>-okon dolgozik.

Megoldás
template<typename TIP>
class Page {
  public:
    Page(int width, int height) : w(width), h(height), page(w*h, '.') {}

    void set(int x, int y, TIP c) {
        if (x >= 0 && x < w && y >= 0 && y < h)
            page[y * w + x] = c;
    }

    void print() const {
        for (int y = 0; y < h; ++y) {
            for (int x = 0; x < w; ++x)
                std::cout << page[y * w + x];
            std::cout << std::endl;
        }
    }

    int width() const {
        return w;
    }
    int height() const {
        return h;
    }

  private:
    int w, h;
    std::vector<TIP> page;
};

A plot() függvény

Adott kód plot() függvénye egy adott rajztáblára, adott karakterekkel kirajzolja a megadott függvényt:

void plot(Page &page, char c, double (*f)(double));

Page p1(75, 20);
plot(p1, 's', sin);

Ez szép és jó, de így a függvények nem paraméterezhetőek. Jobb lenne függvényobjektumokat használni, pl. így:

Linear lin(0.5, 1.7);           // 0.5x + 1.7
Parabolic para(-0.2, 0.5, 2);   // -0.2x² + 0.5x + 2

plot(p1, 'l', lin);
plot(p1, 'p', para);

7. Plot függvénysablon

A probléma egyik megoldása lehet, ha az elképzelt függvény osztályainknak bevezetünk egy közös ősosztályt, a Function-t. Számunkra ez a megoldás most kevéssé érdekes.

Teljesen más megközelítése a problémának, ha a plot() függvény mindent ki tud rajzolni, ami függvényként viselkedik.

Tippek a megoldáshoz:

  • Hogy vehető át paraméterként bármi (ami kirajzolható) a plot() függvényben, beleértve a Linear és Parabolic osztályok példányait? Írd úgy át!
  • Függvénypointerekkel hogyan fog továbbra is működni a kirajzolás?
  • Objektumok (pl. a fenti lin és a para) hogyan tudnak függvényként viselkedni?
Megoldás
  • Az f paraméter típusa legyen sablonparaméter, nevezzük mondjuk FUNC-nak
  • Úgy, hogy a függvénypointer is meghívható függvényként, és (majdan) a kért objektumok is.
  • Overload-olni kell a függvényhívás operátorukat, operator().
template<typename FUNC>
void plot(Page<char> &page, char c, FUNC f) {
    for (int x = 0; x < page.width(); ++x) {
        double fx = (x - page.width()/2)/4.0;
        double fy = f(fx);
        int y = (fy * 4.0) * -1 + page.height()/2;
        page.set(x, y, c);
    }
}

Írd meg a Linear és Parabolic osztályokat is!

8. Plot bármilyen Page-re

Módosítsd a plot() függvényt, hogy ne csak char-okat tartalmazó, hanem bármilyen Page-re tudjon rajzolni! Mi lehet ilyenkor a második paramétere?

Megoldás

Ugyanaz, mint a Page által tárolt típus.

template<typename TIP, typename FUNC>
void plot(Page<TIP> &page, TIP c, FUNC f) {
    for (int x = 0; x < page.width(); ++x) {
        double fx = (x - page.width()/2)/4.0;
        double fy = f(fx);
        int y = (fy * 4.0) * -1 + page.height()/2;
        page.set(x, y, c);
    }
}

9. Deriválás

Egy függvény deriváltja maga is egy függvény. Ha ismerjük az eredeti függvényt, a deriváltját közelíthetjük egy differenciahányadossal, ahol Δx egy nagyon kicsi szám, pl. 0.001:

        f(x+Δx) - f(x)
f'(x) = ––––––––––––––
              Δx      

Írj derivált osztályt, amely egy bármilyen függvényt eltárol, és maga is függvényként használható! Milyen paraméterű ennek az osztálynak a konstruktora?

Parabolic para(-0.2, 0.5, 2);
Derived<Parabolic> der(para);

plot(p1, 'p', para);
plot(p1, 'd', der);

Megoldhatod azt is, hogy a differenciálhányadost is lehessen paraméterezni.

Ennek mintájára készíthetsz további osztályokat is, pl. olyat, amelyik két függvény szorzatát adja, egy függvény eltoltját, vagy szinuszát adja.

Megoldás
template<typename FUNC>
class Derived {
  public:
    explicit Derived(FUNC const& f, double dx = 0.001) : f(f), dx(dx) {}
    double operator()(double x) const {
        return (f(x+dx) - f(x)) / dx;
    }
  private:
    FUNC f;
    double dx;
};

10. A polinom (≥ C++11)

Lett az előbb lineáris (elsőfokú) és parabolikus (másodfokú) függvényünk. Ha lenne Polynomial osztályunk, abból bárhanyadfokú függvényt példányosíthatnánk.

A terv ez:

Polynomial pol{2, 3, 4, 5};      // 2x³ + 3x² + 4x + 5

plot(p1, 'p', pol);

Ehhez a következőket kell tudni (C++11):

  • Lehetséges olyan konstruktort írni, amelyik tetszőlegesen sok, egyforma típusú adattal hívható. Ilyen az std::vector egyik konstruktora is, aminek segítségével úgy inicializálható, mintha tömb lenne:
std::vector<int> v = { 2, 3, 4, 5 };
  • Az ilyen konstruktor nem kerek (), hanem kapcsos {} zárójellel hívható. Ezért van a fenti kódocskában is {} a pol objektum konstruktorparaméterei körül.
  • Ezt a nyelvi elemet inicializáló listának nevezzük. (Ez nem keverendő a konstruktorokban a kettőspont után írt résszel – sajnos ugyanaz a neve mindkettőnek.) Egy saját IntArray osztálynak így írhatnánk ilyen konstruktort:
#include <initializer_list>

class IntArray {
  public:
    IntArray(std::initializer_list<int> ints);
    /* ... */
};

IntArray arr = { 2, 3, 4, 5 };
  • Az eszköz használatához a fordítót C++11 módba kellhet állítani, a Code::Blocks verziójától függően. (Menüben Settings, Compiler, ...)

Ez a nyelvi elem itt jól fog jönni, mert a polinom fokszáma akármekkora lehet. Legjobb ötlet átvenni egy inicializáló listát a konstruktorban, és azt rögtön tovább is passzolni egy std::vector adattag konstruktorának, mert az együtthatókat úgyis el kell tárolni. A vektor mérete később bármikor lekérdezhető – ez lényegében a polinom fokszáma, ami az adott x helyen történő kiértékelésekor kelleni fog.

Írd meg a Polynomial osztályt! Akár a Horner-elrendezést is használhatod.

Megoldás

11. Linear és Parabolic egyszerűbben

Ezt a két osztályt igazából a Polynomial segítségével triviális implementálni:

  • használj privát öröklést és delegálást,
  • figyelj a konstruktorok paraméterezésére!
Megoldás
class Linear : private Polynomial {
  public:
    Linear(double a, double b) : Polynomial{a, b} {}
    using Polynomial::operator();
};

class Parabolic : private Polynomial {
  public:
    Parabolic(double a, double b, double c) : Polynomial{a, b, c} {}
    using Polynomial::operator();
};