Kihagyás

11. gyakorlat

Ismétlés

  • pure virtual
  • virtual desktruktor
  • további öröklődés példák

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.


Utolsó frissítés: 2020-11-16 14:55:39