Útmutató a 9. laborhoz

Felkészülés a laborra

  • Ismételje át a sablonokról, az iterátorokról és a többszörös öröklésről tanultakat (7., 8. és 9. előadás anyaga)!
  • Nézze át 8. labor megoldásokkal kiegészített feladatait!
  • A feladatok magyarázataiban többször hivatkozunk az STL-re (Standard Template Library), amit részletesen a 10. előadás ismertet. Itt most csak annyit kell tudni, hogy az STL a C++ nyelv szabványos sablonkönyvtára, ami számos tárolósablont és függvénysablont implementál. Ilyenek például az I/O-hoz használt adatfolyamok (std::istream és std::ostream), amiket eddig is használtunk, de nem hangsúlyoztuk, hogy megvalósításuk mögött sabblonok állnak, és ezért az STL részei.

A laborfoglalkozás végén a sablonok.hpp fájlt kell feltöltenie a Jporta rendszerbe (9. önellenőrző feladat), hogy megkapja a labor elvégzéséért járó pontot! Legalább az első hét tesztesetet (ELEKESZULT >= 7) hibátlanul meg kell oldania! A feladatok feltöltésére a laborgyakorlatot követő szombat 06:00-ig van lehetősége.

A laborgyakorlat első feladatcsoportjában egyszerű generikus algoritmusokat készítünk. A második feladatcsoportban az előadáson bemutatott fix méretű generikus tömböt alakítjuk át. Az átalakítás lényege, hogy az iterátort nem két pointerrel (p, pe) valósítjuk meg, hanem az iterátorban a következő elem indexét tároljuk. Ezzel hangsúlyozni kívánjuk, hogy az iterátor belső megvalósítási módja legalább annyira magánügy, mint más osztályok belső működése.

1. feladatcsoport (~30') megoldásokkal

  1. Töltse le az előkészített projektet https://git.ik.bme.hu/Prog2/labor_peldak/lab_09, majd nyissa meg a generikus2 alkatalógusban található projektet!
    Röviden elemezze a tesztprogramot (generikus2_teszt.cpp)! Vegye észre:
    • Az ELKESZULT makró értékétől függően egyre több teszteset hajtódik végre.
    • Egyes tesztesetek a globálisan deklarált itomb és dtomb nevű tömbök adatait, mint adatsorozatot dolgozzák fel generikus algoritmusokkal (printEach, szamol_ha_negativ, szamol_ha)
    • A legtöbb teszteset a gtest_lite ellenőrző funkcióit használja és a kiírásokat úgy ellenőrzi, hogy a standard output helyett egy std::stringstream típusú adatfolyamba irányítja az írást, majd az adatfolyam std::string "arcát" felhasználva (többszörös öröklésből) hasonlítja az eredményt az elvárt kimenethez.
  2. Első feladat egy olyan generikus kiró függvény (printEach) elkészítése, ami egy adatsorozat iterátorokkal megadott elemeit vesszővel elválasztva írja ki egymás után szabványos kimenetre! Az iterátorok a szokásos módon balról zárt, jobbról nyílt intervallumot határoznak meg.
    Az iterátor a pointer általánosítása, így a példa pointereket használ.

    Adjon a projekthez egy sablonok.hpp állományt és ebben valósítsa meg a printEach generikus algoritmust! A függvény használatára a ELKESZULT == 1 esetén lát példát a tesztprogramban. A sablonok.hpp állományban állítsa be a ELKESZULT makró értékét 1-re és futtassa a programot!

  3. Ügyeljen arra, hogy referenciaként adja át stream-et!
  4. Módosítsa az elkészült függvénysablont úgy, hogy függvényparaméterként megadható legyen, hogy hova írjon ki (melyik std::ostream-re). Ennek a paraméternek az alapértelmezett értéke std::cout legyen! Állítsa be a ELKESZULT makró értékét 2-re és futtassa a programot!
  5. Az algoritmus kialakításánál ügyeljen arra, hogy a lehető legkevesebb tudást (műveletet) várja el az iterátortól! Összeadást, kivonást, indexelést ne használjon még akkor se, ha ez pointerek esetén lehetséges!
    Az std::string az 5. laboron létrehozott String-hez hasonló tároló.
  6. Módosítsa a függvényt úgy, hogy az is megadható legyen paraméterként, hogy mi legyen az adatokat szeparáló karaktersorozat! A utoljára kiírt adat után ne írjon szeparátort, hanem helyette egy soremelést (std::endl) írjon! A szeparátor alapértelmezett (default) értéke pedig a ", " karaktersorozat legyen!
    Korábban már utaltunk rá, hogy az I/O kezelésnél használt adatfolyamok (stream-ek) fájlok, sőt sztringek is lehetnek. A C++ nyelv szabványos sablonkönyvtárában (STL) definiált std::stringstream osztály mindenütt használható, ahol a std::ostream vagy std::istream használható, ugyanakkor átalakítható szabványos sztringgé (std::string). Ezt használja ki a tesztprogram annak ellenőrzésére, hogy jól valósította-e meg a printEach sablont. Állítsa be a ELKESZULT makró értékét 3-ra és futtassa a programot!
  7. Eddig iterátor helyett pointereket használtunk. A fancy_iterators.hpp állományban megvalósított counting_iterator egy nagyon primitiv iterátor, ami egy számsorozatot ad vissza, és az iterátoroktól elvárt minimális "tudással" rendelkezik. Állítsa be a ELKESZULT makró értékét 4-re! Ekkor ezt az iterátort használja a program. Fordítson és futtasson!
    Ha a printEach sablon paraméterezésénél/megvalósításánál kihasználta, hogy eddig csak pointerekkel teszteltünk, akkor nem fog lefordulni a program! Ennek oka lehet pl., hogy használta a +, vagy - műveletet. Ezeket a counting_iterator nem valósítja meg. Javítsa a hibákat!
  8. Készítsen egy olyan generikus függvényt (szamol_ha_negativ), ami megszámolja, hogy hány negatív elem van egy iterátorokkal megadott adatsorozatban! Tételezze fel, hogy a generikus adatra értelmezett az adat < 0 művelet, amivel megállapítható, hogy az negatív-e. A függvény használatára a ELKESZULT >= 5 esetén lát példát a tesztprogramban. Állítsa be a ELKESZULT makró értékét 5-re és futtassa a programot!
  9. Készítsen egy nagyobb_mint funktor osztályt (függvényként használható osztály), ami megjegyzi a kontsruktorában adott értéket, a függvényhívás operátora pedig igaz értéket ad a megjegyzett értékeknél nagyobb értékekre! Állítsa be a ELKESZULT makró értékét 6-ra és futtassa a programot!
  10. Készítsen egy szamol_ha függvényt, ami azokat az elemeket számolja meg egy iterátorral adott sorozatban, amelyek eleget tesznek a paraméterként átvett predikátumfüggvénynek! A függvény használatára a ELKESZULT >= 7 esetén lát példát a tesztprogramban. A tesztprogram felhasználja az előző feladatban elkészített funktort. Állítsa be a ELKESZULT makró értékét 7-re és futtassa a programot!
  11. Töltse fel az elkészített sablonok.hpp állományt a Jporta rendszerbe (9. labor, önellenőrzés (Sablonok))! Ne felejtse el átemelni a generikus2_teszt.cpp fáljból az ELKESZULT makrót a feladat készültségi fokának megfelelő értékkel! Ha elfelejti, a Jporta ELKESZULT==7 értéket tételez fel.

2. feladatcsoport (~60')

Másolja át az elkészített sablonok.hpp állományt a letöltött laboranyag (https://git.ik.bme.hu/Prog2/labor_peldak/lab_09) genArray3 alkatalógusába és nyissa meg az ott található projektet. A gen_array_iter3.hpp állományban egy fix méretű, iterátoros tömb (Array) sablon található, ami a 8. előadáson bemutatott változattól gen_array_iter.hpp némileg eltér, így a tároló interfésze jobban hasonlít az std::vector interfészére. Az iterator belső megvalósítása is más. Az eltérések részletesen a következők:

  1. A tömb iterátrora nem két pointert, hanem egy indexet (a következő elem indexét) és egy pointert tárol, ami arra objektumra mutat, amire az iterátort alkalmazzuk.
  2. A ténylegesen tárolt elemek számát is tároljuk.
  3. Bevezettünk két új konstruktort:
    • explicit Array(size_t n, const T& value = T()) — n elemű tömböt hoz létre, melynek minden elemét value értékkel tölti fel;
    • Array(InputIterator first, InputIteratort last) — a first és last iterátorokkal megadott adatokkal tölti fel a fix méretű tömböt;
  4. Bevezettünk új tagfüggvényeket:
    • size() — a ténylegesen tárolt elemek számát adja;
    • capacity() — a tároló allokált (esetünkben maximális) kapacitását adja;
    • max_size() — a tároló maximális kapacitását adja;
    • at(size_t) const — a paraméterként kapott indexű elem referenciáját adja. Ha az adott elem nem létezik, out_of_range() kivételt dob. Ez a kivétel az std::exception osztályból származik, így az alaposztállyal egyszerűen elkapható.
    • at(size_t) — a tagfüggvény nem konstans változata kicsit szokatlan dolgot művel: frissíti a tárolt elemek számát. Ilyet a szabványos tárolóknál nem látunk, de itt ez az egyetlen módja az új elem tárolóba helyezésének.

Feladatok:

  1. Elemezze a gen_array_iter3.hpp fájlban megvalósított sablont, és a gen_array_iter3_test.cpp fájlban megvalósított tesztprogramot, ami a ELKESZULT makró értékétől függően egyre több kódrészletet futtat. Az előző feladatcsoportban a makró értéke az adott feladat sorszáma volt, itt most feladat sorszám+10, amit továbbra is a sablonok.hpp állományban kell beállítani. Állítsa a ELKESZULT makró értékét 11-re! fordítson, futtasson!
    Ha nem érti az iterátoros konstruktor működését, vagy esetleg nem érti, hogy miért van egy újabb sablonparaméter, akkor konzultáljon a laborvezetővel!

    A tesztprogram létrehoz három példányt a generikus osztályból. Egyiket egész értékel, a másikat valós értékekkel, a harmadikat pedig C sztringekre mutató pointerekkel tölti fel a konstruktora segítségével.
    Melyik konstruktor futott? Ha nem világos, hogy melyik, tegyen töréspontot a programba és kövesse a végrehajtást!

  2. A létrehozott tömbök adatainak kiírásához az alábbi függvénysablont készítettük el:
    /// Függvénysablon, ami kiírja egy generikus tömb adatait
    /// Feltételezzük, hogy a generikus tömbnek van: 
    ///   - ForwardIteratora, size(), valamint capacity() tagfüggvénye
    /// @param T   - sablon paraméter: iterátoros tömb típus
    /// @param txt - kiírandó szöveg
    /// @param arr - konkrét tömb
    /// @param os  - output steram 
    template <class T>
    void PrintArray(const char *txt, const T& arr, std::ostream& os = std::cout) {
        os << txt << " size=" << arr.size()
             << " capacity=" << arr.capacity() << std::endl;
        os << "\t data=";
    	// kiírjuk az adatokat
    	
    }
    
    A const referenciaként átvett tömbre csak const_iterator alkalmazható. Elemezze, a const_iterator megvalósítását a gen_array_iter3.hpp állományban!

    Másolja be ezt kódrészletet sablonok.hpp állományba és állítsa be a ELKESZULT makró értékét 12-re! fordítson, futtasson! Kiíródtak a tárolt adatok? Egészítse ki a PrintArray sablont, hogy az adatok is kiíródjanak vesszővel elválasztva egymás után! Használja az előző feladatcsoport megoldásakor elkészített PrintEach sablont!

  3. Állítsa be a ELKESZULT makró értékét 13-ra, és fordítsa le a programot!
    Miért nem fordul? Mi hiányzik?
  4. A pos-inkremens operátor megvalósítására az 1. feladatcsoport constant_iterator osztályában is lát példát.
  5. Egészítse ki az iterator osztályt post-inkremens operátorral! Futtassa programot! Ellenőrizze, hogy helyesen működik-e az iterátor. Ha jól működik, állítsa a ELKESZULT makró értékét 14-re! Fordítson és futtasson. A program ekkor létrehoz egy harmadik tömböt is az egészeket tartalmazó tömbből. Vegye észre, hogy az iterátoros konstruktor használta az iterator post-inkremens operátorát!
  6. Állítsa be a ELKESZULT makró értékét 15-re és a megfelelő helyen egészítse ki a tesztprogramot egy olyan kódrészlettel, ami megszámolja, hogy hány 5-nél nagyobb eleme van az egész elemeket tartalmazó tömbnek! Használja az előző feladatcsoportban elkészített szamol_ha és nagyobb_mint sablonokat! Fordítson, futtasson!
  7. Készítsen generikus algoritmust (forEach()), ami két iterátorral megadott adathalmaz minden elemére meghívja a paraméterként kapott függvényt! A függvény visszatérési értéke maga a meghívott függvény legyen!
        template <typename InputIterator, class Func>
        Func forEach(InputIterator first, InputIterator last, Func func);
    

    Elemezze a tesztprogramot ELKESZULT >= 16 esetre! A tesztkódban a forEach sablon 3. paramétere egy egyszerű funktor (ld. program elején), ami a függvényparaméterként kapott egész értéket hozzáadja a konstruktorban kapott értékhez. Mivel a forEach visszatérési értéke maga a paraméterként átadott funktor, így a sorozat összege abból kinyerhető.

    Felmerülhet a kérdés, hogy miért nem referencia paraméterrel adtuk át a funktort a forEach sablonnak, hiszen az összeg kézenfekvőbben elérhető lett volna. Az ok egyszerű: A függvények hívásakor a paraméterlistán létrehozott objektum referenciával csak konstansként adható át, azaz vagy külön kell létrehozni a funktort (nem a paraméterlistán), vagy az nem változtathatja magát. Azzal, hogy a visszatérési értékként adtuk vissza az objektumot, ezt a problémát megkerültük.

    Állítsa be a ELKESZULT makró értékét 16-ra! Fordítson, futtasson!

  8. A korábban készített printEach() függvénysablon helyett a forEach() sablonnal írassa ki az intarr1 tömb adatait! (Az utolsó adat kiírása után is lehet vessző.)
    Ötlet: Készítsen egy olyan osztályt (ostreamfunktor), aminek a konstruktora megjegyzi, hogy melyik stream-re kell írni, és mi a határoló karaktersorozat. A függvényhívás operatora pedig írja a paraméterét a korábban megjegyzett stream-re, majd írja ki a határoló karaktersorozatot. Állítsa be a ELKESZULT makró értékét 17-re! Fordítson, futtasson!
  9. Töltse fel az elkészített sablonok.hpp állományt a Jporta rendszerbe (9. labor, önellenőrzés (Sablonok))! Ne felejtse el átemelni a gen_array_iter3_test.cpp fáljból az ELKESZULT makrót a feladat készültségi fokának megfelelő értékkel! Ha elfelejti, a Jporta ELKESZULT==7 értéket tételez fel.
  10. IMSC feladat: Módosítsa az Array osztály konstruktorait úgy, hogy azok a tömbelemek, amelyek nem kaptak értéket, mindenképpen inicializálva legyenek! Elemi típus esetén a kezdeti érték 0 legyen, azaz a következő kódrészlet 8 db nullát írjon ki!
        Array<int, 20> t(0);
        t.at(8) = 12;
        for (size_t i = 0; i < 8; i++)
           cout << t.at(i) << " ";   
    
    Nem elemi típusokra ez automatikusan teljesül, hiszen az osztályban levő tömb minden elemére meghívódik a default konstruktora. Elemi típusok esetén azonban ez nem történik meg. Ezt illusztrálja az Integer osztállyal létrehozott tömb (ELKESZULT >= 18)


    Oldja meg, hogy a nem elemi típusok ne inicializálódjanak 2-szer! A megoldáshoz használja fel az isClass sablont!

Kiegészítő olvasmányként ajánlom Izsó Tamás sablonokkal kapcsolatos összefoglalóját: http://www.hit.bme.hu/~izso/sablon.pdf. Rafinált trükkökre (pl. template paraméter típusának eldöntése) ZH-n nem lesz szükség, de tudni kell egyszerű sablonokat készíteni.

Jó munkát!

Szeberényi Imre

Utolsó frissítés: 2021-04-26 06.35