Kihagyás

9. gyakorlat

Ismétlés

Hibakezelés

A programok futás közben különféle hibákba futhatnak, amik egy részére felkészíthetjük programunkat. Lehetnek rendszerhez kapcsolódó hibák, például elfogy a memória, nincs megfelelő jogosultság egy erőforráshoz vagy akár felhasználói hiba, például amikor szám helyett szöveget adnak meg a konvertáló függvénynek. Ezeket a hibákat kezelni kell, különben definiálatlan viselkedéssel folytatódhat tovább a program futása.

Hibakezelésre láttunk már példát a stringek számmá konvertálásakor és a dinamikus memóriakezeléáskor. Ekkor a hibát egy Exception jelezte, hogy a program végrehajtása során hiba lépett fel. Ekkor nem fut le teljesen a függvény (megszakad a futás a hiba eldobásakor), nem adja vissza a megfelelő értéket, hanem jelzi, hogy adott egy objektum, az tartalmazza a hiba részleteit, onnantól azzal kell foglalkozni.

Az ilyen megjelenő objektumokat try-catch párossal tudjuk kezelni. Minden a try ágban lévő hibát a catch ágban/ágakban tudunk kezelni. Például:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <string>
int main() {
    try {

        int a = std::stoi("nem integer");
        /*
        Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
        */
    }
    catch(const std::invalid_argument& error) {
        std::cerr << "A hiba konvertalas soran: " << error.what() <<std::endl;
    }
}

Látható, hogy a try ágban megpróbálunk végrehajtani egy feladatot, de hibát kapunk. A catch ágban azonban több részletet is ki kel emelni:

  • const: A catch ág egy objektumot kap el ami lehet konstans, hiszen nem akarjuk megváltoztatni a hiba jellemzését, ezért amikor elkapjuk const típust kell megadni
  • referencia: A hiba objektumok gyakran osztályok (erről később) ezért lehet öröklődési viszony köztük. Ahhoz, hogy az ilyen öröklődés polimorfikusan tudjon viselkedni, referenciát kell használnunk.
  • what metódus: az előre beépített hibáknak van egy meghatározott metódusuk, amely jellemzi azokat. Ez a what.

std::exception

Legtöbbször a hibáknak külön osztályt hozunk létre. Ekkor ezeket az std::expection osztályból származtatjuk le. Ennek az osztálynak a lényege, hogy polimorfikusan általánosítsa a hibákat, itt található a virtuális what metódus. Mivel polimorfikusan használhatók a típusok, az előbbi példát std::exception típussal is elkaphattuk volna:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>
#include <string>
int main() {
    try {

        int a = std::stoi("nem integer");
        /*
        Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
        */
    }
    catch(const std::exception& error) {
        std::cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<std::endl;
    }
}

Látható, hogy így is az 'stoi' üzenetet kapjuk, hiszen még mindig az std::invalid_argument hiba dobódott, csak polimorfikusan kezeltük.

Referencia használata nélkül azonban csak az 'std::exeption' üzenetet kapnánk, hiszen a polimorfizmus nem működik érték szerinti átadás során. ROSSZ HASZNÁLAT:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream>
#include <string>
int main() {
    try {

        int a = std::stoi("nem integer");
        /*
        Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
        */
    }
    // FONTOS, HOGY ÉRTÉK SZERINT KAPTA EL A HIBÁT. HELYTELEN!
    catch(const std::exception error) {
        std::cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<std::endl;
    }
}

warning: catching polymorphic type ‘const class std::exception’ by value [-Wcatch-value=] catch(const std::exception error) {

Több catch ág

Több catch feltételt is írhatunk a try ághoz, hiszen több féle hiba is felléphet egy-egy művelet során. Ekkor ezek lineárisan kerülnek tesztelésre, és fontos, hogy az eddig tanultakhoz hasonlóan az első illeszkedő kivételkezelő fogja kezeli a hibát (nem pedig a legjobban illeszkedő, tehát ha egyszer kapunk egy gyerek típusú hibát, de az őst elkapó kivételkezelő van a kódban előrébb, akkor minden esetben az ősé fog lefutni).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
int main() {
    try {

        int a = std::stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
        /*
        Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
        */
    }
    catch(const std::invalid_argument& error) {
        std::cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<std::endl;
    }
    catch(const std::out_of_range& error) {
        std::cerr << "A hiba kovertalas soran (masodik catch ag): " << error.what() <<std::endl;
    }
    catch(const std::exception& error) {
        std::cerr << "Altalanos exception kezeles: " << error.what() << std::endl;
    }
}

Látható, hogy a második catch ágban folytatódik a program futása, hiszen az std::invalid_argument exception polimorfikusan sem tudja kezelni az éppen eldobott std::out_of_range exceptiont. Ekkor továbbhalad a hiba a következő catch ágra, ami már tudja kezelni, így onnan nem halad tovább.

Látható, hogy felkészültünk egyéb hibák kezelésére is, nem csak a két konverziós hibára. Ha ezt rossz helyre írjuk, akkor információt veszíthetünk. Ha előre írnánk, akkor minden hibát polimorfikusan elkapna az std::exception így a specifikus catch ágakba sosem kerülne a vezérlés.

ROSSZ PÉLDA:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
int main() {
    try {

        int a = std::stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
        /*
        Kod a-val ami lefut ha minden jol ment, hiba eseten mar nem.
        */
    }
    catch(const std::exception& error) {    // ROSSZ! Minden hibat elkap polimorfikusan
        std::cerr << "Altalanos exception kezeles: " << error.what() << std::endl;
    }
    catch(const std::invalid_argument& error) {
        std::cerr << "A hiba konvertalas soran (exception tipussal): " << error.what() <<std::endl;
    }
    catch(const std::out_of_range& error) {
        std::cerr << "A hiba kovertalas soran (masodik catch ag): " << error.what() <<std::endl;
    }
}

warning: by earlier handler for ‘std::exception’ catch(const std::exception& error) { // ROSSZ! Minden hibat elkap polimorfikusan

Saját hiba típus

Saját hibát is definiálhatunk és hasonlóan általánossá tehetjuk az eddig ismert hibákhoz, ha azt az std::exception osztályból (vagy egy leszármazottjából) származtatjuk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class egyszeru_sajat_hiba : public std::exception {
    const char* what() const noexcept override {
        return "Ez egy egyszeru sajat hiba!";
    }
};

class sajat_hiba : public std::exception {
    std::string message;
    int rossz_szam;
public:
    sajat_hiba(const std::string& message, int bad_number = 0) : message(message), rossz_szam(bad_number) {}

    const char* what() const noexcept override {
        return message.c_str();
    }

    int get_bad_number() const {
        return rossz_szam;
    }
};

void hiba_kiiratas(const std::exception& e) {
    std::cerr << e.what() << std::endl;
}

int oszt(int elso, int masodik) {
    if (masodik < 0) {
        throw sajat_hiba("Nullaval valo osztas", masodik);
    }
    return elso / masodik;
}

int main() {
    egyszeru_sajat_hiba esh;
    hiba_kiiratas(esh);

    sajat_hiba sh("Ez a sajat hibam. Keszen all a hasznalatra");
    hiba_kiiratas(sh);

    try {
        oszt(342, -34);
    } catch (sajat_hiba& exception) {
        cerr << exception.what() << endl;
        cerr << "A hibat az alabbi szam okozta: " << exception.get_bad_number() << endl;
    }
}

Az egyszeru_sajat_hiba típus csak a beépített what metódust írja felül, hogy a hiba kiíratásakor megjelenő szöveg az általunk megadott legyen (ez megoldható volna az exception példányosításával, azonban így lesz saját hibatípusunk, így saját hibakezelőt is írhatunk az általunk definiált típushoz). Mivel a sajat_hiba már általános std::exception-ként is használható csak a használata marad hátra. Nem elegendő megkonstruálnunk, el is kell dobni. Az eddigieken kívül természetesen a hiba objektumok is ugyanolyan objektumok, mint bármelyik másik, lehetnek adattagjai, különböző konstruktorai, metódusai (ahogy látjuk a gettereket is).

noexcept kulcsszó

A what felüldefiniálása során meg kellett adni egy noexcept nevű tulajdonságot. Ez azt jelzi, hogy a metódus biztosan nem fog hibát eredményezni, nem kerül sor újabb exception eldobására annak futása során.

throw kulcsszó

Ha egy folyamat végrehajtása során hibába fut a program, a programozónak kell megoldania, hogy egy hiba eldobásra kerüljön. Ezt a throw kulcsszóval tehetjük meg. Meg kell adni, hogy mi az amit el szeretnénk dobi. Ez lehet pl. a sajat_hiba egy példánya.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

class sajat_hiba : public std::exception {
    std::string message;
public:
    sajat_hiba(const std::string& message) : message(message) {}

    const char* what() const noexcept override {
        return message.c_str();
    }
};

void foo() {
    // sok-sok futas

    // hiba:
    throw sajat_hiba("Valami hibas volt foo futasa kozben.");
}

int main() {
    try {
        foo();
    }
    catch(const sajat_hiba& sh) {
        std::cerr << sh.what() << std::endl;
    }
}

Nem osztály hibák

A hibakezelés elején említésre került, hogy általában osztálytípusúak a hibák, azonban ez C++ esetében nem feltétlenül kell így legyen (Java esetén csak így lehetett). Bármit el lehet dobni, bármilyen értéket; legyen az _const char, _std::string, int, stb. Ezeket is el lehet kapni, azonban ekkor már nem használható az exception elkapása, hiszen ezek nem leszármazottjai az std::exception* osztálynak (és nem is konkrétan exception típusúaka). Ekkor a típusukat kell használnunk. Természetesen ezeket is elkaphatjuk referencia szerint, azonban primitív típusok esetén ez nem szükséges.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
int main() {
    try {
        throw 42;
    }
    catch(const std::exception& e) { // Nem fut le, hiaba a legelso!
        std::cerr << "A hiba: " << e.what() << std::endl;
    }
    catch(int i) {
        std::cerr << "A hiba: " << i << std::endl;
    }

    try {
        throw "Ez egy const char* tipusu literal";
    }
    catch(const char* hiba) {
        std::cerr << hiba << std::endl;
    }
}

Minden hiba kezelése

Látható, hogy egyes hibák nem tartoznak az std::exception alá, így általánosan nem kezelhetők. Azonban ha szeretnénk megoldani, hogy biztosan elkapjunk minden hibát, minden lehetséges hibatípusra kell catch ágat írnunk. Azonban ez nem biztos, hogy minden esetben lehetséges (vagy azért mert rengeteg féle-fajta hiba van, amiket azonosan akarunk kezelni, vagy pedig azért mert nem is tudjuk pontosan, hogy milyen hibákra kell felkészíteni a programunkat). Azonban ezt is megoldhatjuk, ha elkapáskor a típusok helyett egyszerűen csak ... (három pontot) írunk. Ezzel bármit el tudunk kapni a catch ágban.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
int main() {
    try {
        throw 42;
    }
    catch(const std::exception& e) {    // Nem fog lefutni
        std::cerr << e.what() << std::endl;
    }
    catch(...) {
        std::cerr << "Ismeretlen hiba" << std::endl;
    }

    try {
        throw "Hibauzenet";
    }
    catch(const std::exception& e) {    // Nem fog lefutni
        std::cerr << e.what() << std::endl;
    }
    catch(...) {
        std::cerr << "Ismeretlen hiba" << std::endl;
    }
}

RAII (Resource acquisition is initialization)

Ez a fogalom annyit jelent, hogy az erőforráso foglalás az inicializálás során történik meg. A név ha nem is a legegyértelműbb, széles körben elterjedt.

Használata a hibakezeléshez köthető, méghozzá erőforrás használatakor. Legyen erre példa a következő:

  • try ágban foglaljunk erőforrást

  • hajtsuk végre a try ágat

  • exception dobódik

  • catch ág kezeli a hibát

Ekkor azonban a catch ágra ugrik azonnal a vezérléás, több esetén csakis az egyikre. Ha a try blokk végén volt erőforrás felszabadítás, az nem futott le. Javaban erre a megoldás a finally ág volt, mely minden esetben, hibátlan esetleg hibás (bármilyen) lefutás esetén végrehajtásra került; ezzel biztosítva az erőforrás felszabadítását. Ilyen C++ esetében nincsen, azonban pont erre megoldás a RAII és a feltételezés, hogy az erőforrások objektumok, melyek konstruktorral kerülnek inicializálásra és fontosabb, hogy destruktor hívódik megszűnésükkor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <iostream>

/**
* Az osztaly portokat foglal le hasznalatra.
* Az egyszeruseg kedveert ezt csak szamokkal fogjuk jelolni.
* Amig a szamok nem nullak, addig van lefoglalt port.
*/
class halozati_eroforras {
    // tegyuk fel, hogy ez nem egyszeru int, hanem az OS altal biztositott eroforras a portokhoz.
    unsigned hasznalt_portok = 0;
    public:
    halozati_eroforras(unsigned portok) : hasznalt_portok(portok) {}
    ~halozati_eroforras() {
        // felszabaditjuk a portokat
        // most csak beallitjuk a 0 erteket
        hasznalt_portok = 0;
        std::cout << "Portok felszabaditva" << std::endl;
    }
    unsigned get_hasznalt_portok() const { return hasznalt_portok; }
};

class sajat_hiba : public std::exception {
    std::string message;
    int rossz_szam;
public:
    sajat_hiba(const std::string& message, int bad_number = 0) : message(message), rossz_szam(bad_number) {}

    const char* what() const noexcept override {
        return message.c_str();
    }

    int get_bad_number() const {
        return rossz_szam;
    }
};

int main() {

    try {
        // foglaljuk le a portokat!
        // ezt egy objektum inicializalasaval tehetjuk meg.
        halozati_eroforras eroforras(4);

        std::cout << eroforras.get_hasznalt_portok() << std::endl;

        // egyeb eroforras hasznalat..
        // hiba lep fel
        throw sajat_hiba("A hiba oka: ...", 42);

        // az eroforrast itt kellene felszabaditani, de ide mar nem jut el a futas
    }
    catch(const sajat_hiba& sh) {
        std::cerr << sh.what() << std::endl;
        // hiba kezelese..
    }
    catch(const std::exception& error) {
        // hiba kezeles..
    }

    // Itt mar nem letezik az eroforras valtozo, igy most az objektum sem
}

Látható, hogy a try blokkban erőforrást egy objektum létrehozásával foglalunk, majd használjuk, de a felszabadítás előtt hiba exception dobódik. Ennek ellenére nincsen szükség finally ágra, hiszen a destruktor láthatóan így is lefutott. Mivel a try blokkot elhagytuk, a catch ágba kerültünk, így az objektum megszűnt, ami a destruktor lefutását eredményezte.

Smart pointerek

A pointerek használatának egy jelentős hátránya, hogy a memóriafelszabadítást a fejlesztőnek kell megoldania és egy - egy felszabadítás hiánya jelentős problémát okozhat. A smart pointerek használata ezt a felelősséget könnyíti, hiszen a memória foglalás és felszabadítás automatikusan történik.

A smart pointerek úgynevezett template osztályok. Számunkra ez azt jelenti, hogy használatukkor meg kell adni, hogy milyen típussal akarjuk használni a smart pointert '<' és '>' jelek között. Használatukhoz a memory header-t kell include-olni.

Pointerek használatakor a következő folyamatosak a lényegesebbek: - memóriafoglalás - másolás - memóriafelszabadítás

Smart pointerek használatával magát a pointert egy wrapper osztályba zárjuk és az adott osztály példányosításával hozunk létre pointert. Tehát ezek az objektumok nem dinamikus memóriát használó típusoknak értelmezendők (mint pl. Kurzus ahol a Hallgatókat tároltuk), hanem egy konkrét pointer típus helyett használandók. Az osztályoknak meg vannak valósítva a * és -> operátorai, így a szokványos pointer szintaxissal használhatjuk.

Memóriafoglalás: A smart pointer objektum példányosításakor lehetőség van memóriafoglalásra. (A smart pointerhez egyéb módon is foglalható memória, most csupán a konstruktoron keresztüli foglalást tárgyaljuk.) A memóriafoglalás után a smart pointer folyamatosan kezeli a memóriát.

Memóriafelszabadítás: Smart pointer objektumoknak is van élettartama, akárcsak egy általános objektumnak. Mikor egy objektum megszűnik, azt többé nem hivatkozhatjuk, így egy pointer esetében megszűnése előtt fel kell szabadítanunk a memóriát, különben meglévő referencia nélkül nem tudjuk, hogy melyik memóriaterületet kellene felszabadítani. Az objektum megszűnésekor lefut a destruktor, így a smart pointer automatikusan felszabadítja a foglalt memóriát.

Másolás: A dinamikus memória résznél a legtöbb feladatot a másolás okozott, hiszen deep-copy-t szerettünk volna elérni. Pointer másoláskor alap esetben nem foglalunk memóriát, hiszen csak értékeket állítunk be. (Ezért nagyon fontos, hogy nem dinamikus memóriát használó objektumról van szó, hanem pointer helyettesítőről.) A másolás viselkedése assignemnt operátor és másoló konstruktor használatával adható meg.

Unique pointer

Gyakran arra van szükség, hogy egy memóriaterületet csak egy változón keresztül érhessünk el. Ekkor std::unique_ptr típust használhatunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <memory>
struct Wrap {
  unsigned i = 0;
  Wrap( unsigned i ) : i( i ) {}
  Wrap() = default;
};

int main() {
  std::unique_ptr<Wrap> ptr( new Wrap( 2 ) ); // memoriafoglalas, Wrap-ban a 2-es ertek
  std::unique_ptr<Wrap[]> ptr2( std::make_unique<Wrap[]>( 10 ) ); // memoriafoglalas - 10 elem.

  // masolas
  // ptr2 = ptr; //forditasi hiba
  // masolni nem lehet

  // objektum megszunik.
  // memoriat nem kell kezzel felszabaditani.
  // objektum destruktor megoldja a felszabaditast.
}

A fenti példában is látható, hogy a másolás nem megengedett unique pointer esetében, ezzel is biztosítva, hogy egy memóriacím nem tartozik két objektumhoz (változó). A másolás azonban gyakran igény ekkor is, - legyen ez egy ideiglenes memória beállításakor -, így ezt is meg kell oldani. Ebben az esetben a feladatunk az eredeti foglalt memória felszabadítása és a memóriacím átállítása volt. Amire nem kellett figyelni, hogy a lemásolandó elemet többet ne tudjuk használni a memória elérésére.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>
#include <iostream>
struct Wrap {
  unsigned i = 0;
  Wrap( unsigned i ) : i( i ) {}
  Wrap() = default;
};

int main() {
  std::unique_ptr<Wrap> ptr( new Wrap( 2 ) ); // memoriafoglalas, Wrap-ban a 2-es ertek
  std::unique_ptr<Wrap> ptr2( std::make_unique<Wrap>( 10 ) ); // memoriafoglalas - Wrap-ban a 10-es ertek.

  std::cout << (ptr2.get() == nullptr) << std::endl; // a tarolt pointer null-e? Nem.
  // masolas
  ptr = std::move( ptr2 );

  std::cout << (ptr2.get() == nullptr) << std::endl; // a tarolt pointer null-e? Igen, nem erjuk el a memoriat ezzel.

  // objektum megszunik.
  // memoriat nem kell kezzel felszabaditani.
  // objektum destruktor megoldja a felszabaditast.
}

Shared pointer

Shared pointer esetében azt várjuk, hogy a memóriacímet több objektum (változó) is hivatkozhassa. Ekkor a megoldandó feladat, hogy a memória csak akkor legyen felszabadítva, ha már egy objektum (változó) sem hivatkozza a memóriacímet. Shared pointer esetében ez megoldott, így ezzel nem kell foglalkoznunk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <memory>
#include <iostream>
struct Wrap {
  unsigned i = 0;
  Wrap( unsigned i ) : i( i ) {}
  Wrap() = default;
};
void create_and_assign( std::shared_ptr<Wrap>& variable_to_assign ) {
  std::shared_ptr<Wrap> shared1( new Wrap( 10 ) ); // memoriafoglalas.
  variable_to_assign = shared1; // pointer masolas.
  // itt az első objektum megszunik.
}

int main() {
  std::shared_ptr<Wrap> shared2;
  create_and_assign( shared2 );
  // itt ha az elso objektum megszunese felszabaditotta volna a memoriat, hibat kapnank.
  std::cout << shared2->i << std::endl;
}

Utolsó frissítés: 2022-11-25 12:34:01