Ú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
- 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.
- 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!
- 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!
- 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! - 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!
- 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!
- 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!
- 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!
- 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:
- 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.
- A ténylegesen tárolt elemek számát is tároljuk.
- 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;
- 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:
- 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! - 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!
-
Állítsa be a ELKESZULT makró értékét 13-ra, és fordítsa le a programot!
Miért nem fordul? Mi hiányzik? -
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!
- Á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!
- 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!
- 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! - 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.
- 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