Erindid
Pärast selle praktikumi läbimist üliõpilane
- teab, mis on erindid ja millal on neid vaja kasutada
- oskab defineerida erindeid ja neid vajadusel visata
- oskab vajadusel erindeid käsitleda
Erindite (exception) töötlemine
C++ keeles on võimalik kasutada erindeid (exceptions) programmi tõrgete ja erandlike olukordade käsitlemiseks. Erindid võimaldavad programmi töö ajal teatud vigu ja tõrkeid tuvastada ja neile reageerida, ilma et programm täielikult katki läheks.
Erindeid saab defineerida kasutades nii standardseid erindeid (nt exception
) kui ka kohandatud erindeid (mis on spetsiaalselt loodud programmi vajadustele). Erindite kasutamine toimub järgmiselt:
1. Erindi defineerimine. Näiteks saab defineerida erindi MinuErind
, millel on string
tüüpi isendimuutuja m_sõnum
, järgmiselt:
class MinuErind{ public: MinuErind(string sõnum) : m_sõnum{sõnum}{} string m_sõnum{}; };
2. Erindi viskamine. Erindi viskamiseks kasutatakse võtmesõna throw
ja erindi objekti. Näiteks saab visata erindi MinuErind
järgmiselt:
if (y == 0) { throw MinuErind("Jagamine nulliga!"); }
3. Erindi käsitlemine. Erindi käsitlemiseks kasutatakse try-catch
plokki. try
plokis määratakse kood, mis võib tekitada erindeid ja catch
plokis määratakse, kuidas erinditele reageerida. catch
plokki nimetatakse ka püüniseks. Näiteks saab käsitleda erindit MinuErind
järgmiselt:
try{ if (y == 0){ throw MinuErind("jagamine nulliga!"); } } catch (MinuErind& e){ cout << "Viga: " << e.m_sõnum << '\n'; }
Paneme nüüd kogu koodi kokku:
#include <iostream> #include <string> using namespace std; class MinuErind { public: MinuErind(string sõnum) : m_sõnum{sõnum} {} string m_sõnum{}; }; int main() { int x{1}, y{}, z{}; try { if (y == 0) { throw MinuErind("jagamine nulliga!"); } z = x / y; } catch (MinuErind& e) { cout << "Viga: " << e.m_sõnum << '\n'; } return 0; } | Viga: jagamine nulliga! |
Käsk throw
võib visata ka lihtsalt täisarvu. Oluline on, et leiduks catch
plokk, kus täisarv kinni püütakse. Järgnev näide illustreerib programmi täitmise järjekorda, kus erindite püüdmine on mitmel tasandil. Funktsioon f1()
pöördub funktsiooni f2()
poole, see omakorda f3()
poole, kus visatakse erind ja püütakse ka kinni.
#include <iostream> using namespace std; void f1(); // Funktsioonide deklaratsioonid void f2(); void f3(); void erindiviskaja() { cout << "erindiviskaja() alustab\n"; throw 0; // cout << "erindiviskaja() lõpetab\n"; } int main() { try { f1(); cout << "main peale f1() täitmist\n"; } catch (int ex) { cout << "Erindi töötlemine main()-is\n"; } cout << "main() lõpetab\n"; return 0; } // Definitsioonid void f1() { cout << "f1() alustab\n"; try { f2(); cout << "f1()-s peale f2() täitmist\n"; } catch (int ex) { cout << "Erindi töötlemine f1()-s\n"; } cout << "f1() lõpetab\n"; } void f2() { cout << "f2() alustab\n"; try { f3(); cout << "f2()-s peale f3() täitmist\n"; } catch (int ex) { cout << "Erindi töötlemine f2()-s\n"; } cout << "f2() lõpetab" << endl; } void f3() { cout << "f3() alustab" << endl; try { erindiviskaja(); cout << "f3()-s peale erindiviskamist\n"; } catch (int ex) { cout << "Erindi töötlemine f3()-s\n"; } cout << "f3() lõpetab" << endl; } | f1() alustab f2() alustab f3() alustab erindiviskaja() alustab Erindi töötlemine f3()-s f3() lõpetab f2()-s peale f3() täitmist f2() lõpetab f1()-s peale f2() täitmist f1() lõpetab main peale f1() täitmist main() lõpetab |
Funktsioonis f3()
peale erindiviskamist (erindiviskaja()
) läheb juhtimine catch
-plokki ja käsku cout << "f3()-s peale erindiviskamist\n"
ei täideta. Peale seda täidetakse catch
plokile järgnev käsk. Kuna erind on töödeldud, siis funktsioonis f2()
jätkub töö normaalselt, st käsust peale f3()
poole pöördumist, analoogiliselt funktsioonides f1()
ja main()
.
Erindeid ei pea kohe viskamise juures töötlema, vaid selle võib jätta hilisemaks. Muudame funktsiooni f3()
järgmiselt:
void f3() { cout << "f3() alustab" << endl; erindiviskaja(); cout << "f3()-s peale erindiviskamist\n"; cout << "f3() lõpetab" << endl; }
Nüüd funktsioonis f3()
erindit ei töödelda, see delegeeritakse pöördujale, st funktsioonile f2()
. Funktsiooni töö tulemuseks on
f1() alustab f2() alustab f3() alustab erindiviskaja() alustab Erindi töötlemine f2()-s f2() lõpetab f1()-s peale f2() täitmist f1() lõpetab main peale f1() täitmist main() lõpetab
Kui erindi töötlejat ei leita, siis programm lõpetab töö veaga. Kui eemaldada erinditöötlus ka funktsioonidest f2()
, f1()
ja main()
, siis programm lõpetab veateatega
terminate called after throwing an instance of 'int'
Mõnikord on vaja püünises (catch
-plokis) visata uuesti sama erind. Seda saab teha operaatoriga throw
. Järgmises näites on erindiklassiks klass näita
, kus on konstruktor, koopiakonstruktor ja destruktor; lisaks staatiline väli number
, mille abil loetakse klassist tehtud objekte (ka koopiaid). Funktsioon loenda
on rekursiivne ja erind visatakse parameetri n = 1
korral. Püünises visatakse uuesti sama erind operaatoriga throw
.
#include <exception> #include <iostream> #include <string> using namespace std; /// Näitab objektide loomist (konstruktor) ja hävitamist (destruktor) class näita { public: // konstruktor näita(string const &mis) : m_id{number}, m_mis{mis} { print(" näita konstruktor "); ++number; } // koopiakonstruktor näita(näita const &ex) : m_id{number}, m_mis{ex.m_mis} { print("näita koopiakonstruktor "); ++number; } // destruktor ~näita() { print("~ näita destruktor"); } void print(string const & silt) const{ cout << silt << " (" << m_mis << ": " << m_id << ")\n"; } private: static int number; // loendur, mitmes objekt tehakse int m_id; string m_mis; }; int näita::number = 1; //staatilise välja algväärtustamine void loenda(int n) { cout << "algab loenda(" << n << ")\n"; try { if (n == 1) throw näita(" exception "); else if (n > 0) loenda(n - 1); } catch (näita& ex) { // edastatakse viite abil cout << "catch n = " << n << '\n'; throw; // viskab uuesti sama erindi } cout << "loenda lõpp (" << n << ")\n"; } int main() { try { loenda(3); } catch (näita ex) { // edastatakse koopia abil ex.print("catch main "); } cout << "Kõik valmis!\n"; } | algab loenda(3) algab loenda(2) algab loenda(1) näita konstruktor ( exception : 1) catch n = 1 catch n = 2 catch n = 3 näita koopiakonstruktor ( exception : 2) catch main ( exception : 2) ~ näita destruktor ( exception : 2) ~ näita destruktor ( exception : 1) Kõik valmis! |
Seda erindit asutakse püüdma funktsiooni väljakutsete magasinis, kus püünises visatakse iga kord erind uuesti. Funktsioonis loenda
on püünises erindi edastamine viite abil catch (näita& ex)
, aga funktsioonis main
koopia abil catch (näita ex)
, st toimub koopiakonstruktori poole pöördumine ja koopia hiljem hävitatakse. Esialgselt visatud erind kustutatakse alles main
funktsioonis.
Kui funktsioonis loenda
kasutada erindi edastamist koopia abil catch (näita ex)
, siis tehakse edastamiseks erindist iga kord koopia ja püünises see koopia hävitatakse:
algab loenda(3) algab loenda(2) algab loenda(1) näita konstruktor ( exception : 1) näita koopiakonstruktor ( exception : 2) catch n = 1 ~ näita destruktor ( exception : 2) näita koopiakonstruktor ( exception : 3) catch n = 2 ~ näita destruktor ( exception : 3) näita koopiakonstruktor ( exception : 4) catch n = 3 ~ näita destruktor ( exception : 4) näita koopiakonstruktor ( exception : 5) catch main ( exception : 5) ~ näita destruktor ( exception : 5) ~ näita destruktor ( exception : 1) Kõik valmis!
Seega mõistlik on võimalusel kasutada erindi edastamiseks viidet.
Teegis <exception>
on C++ erindite klassid, mida saab erindite töötlemisel kasutada. Toome erindite klassi hierarhiast olulisema osa:
Oma erindiklassi saab pärida ka sisseehitatud klassist. Järgmises näites visatakse erind peale negatiivse arvu sisestamist. Programmi jooksutati kaks korda.
#include <exception> #include <iostream> #include <string> #include <vector> using namespace std; class MinuErind : public exception { public: MinuErind(string sõnum, int arv) : m_sõnum{sõnum}, m_arv{arv} {} string getSõnum() { return m_sõnum; } int getArv() { return m_arv; } private: string m_sõnum{}; int m_arv{}; }; int main() { int x{}; try { cout << "Sisesta positiivne täisarv:\n"; cin >> x; if (x < 0) { throw MinuErind("Sisestasid negatiivse arvu!", x); } } catch (MinuErind e) { cout << "Püüti MinuErind: " << e.getSõnum() << ": " << e.getArv(); return 1; } return 0; } | Sisesta positiivne täisarv: 2 Process finished with exit code 0 Sisesta positiivne täisarv: -5 Püüti MinuErind: Sisestasid negatiivse arvu!: -5 Process finished with exit code 1 |
noexcept
Mõned funktsioonid ei tohiks kunagi erindit tekitada. Näiteks allolevas klassis A
funktsioon getA
, mis tagastab täisarvu. Sellisele funktsioonile võib lisada täiendi noexcept
. Kui kompilaator teab, et funktsioon ei viska kunagi erindit, loob ta efektiivsema objektikoodi.
class A{ int m_a{}; public: A(int a) : m_a{a}{} int getA() noexcept{ return m_a; } };
Täpsemalt saab erindite kasutamist uurida aadressil