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
- oskab kasutada klassimalli
optional
Sisukord
1. Erindi defineerimine | 4. Erindite standardteek | 7. Klassimall optional |
2. Erindi viskamine | 5. Erindiklassi pärimine standardteegist | Enesetestid |
3. Erindi uuesti viskamine | 6. Võtmesõna noexcept |
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:
Erindi defineerimine
Näiteks saab defineerida erindi MinuErind
, millel on string
tüüpi isendimuutuja m_sõnum
, järgmiselt:
class MinuErind{ public: string m_sõnum{}; MinuErind(string sõnum) : m_sõnum{sõnum}{} };
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!"); }
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 erindile reageerida. catch
plokki nimetatakse ka püüniseks. Neid plokke võib olla mitu. Kui try
plokis viga ei teki (erindit ei visata), siis jäetakse catch
plokid vahele.
Näiteks saab käsitleda erindit MinuErind
järgmiselt:
try{ if (y == 0){ throw MinuErind("jagamine nulliga!"); } } catch (const 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: string m_sõnum{}; MinuErind(string sõnum) : m_sõnum{sõnum} {} }; int main() { int x{1}, y{}, z{}; try { if (y == 0) { throw MinuErind("jagamine nulliga!"); } z = x / y; } catch (const MinuErind& e) { cout << "Viga: " << e.m_sõnum << '\n'; } return 0; } | Viga: jagamine nulliga! |
Vaatame nüüd, kuidas toimub programmi täitmine, kui peale try
plokki on mitu catch
plokki (püünist) järjest. Sellisel juhul vaadatakse catch
plokid järjest läbi ja täitmisele tuleb esimene, mille parameeter sobib. try
plokis defineeritud objektidele kutsutakse välja destruktorid. Kui valitud catch
ploki täitmine sai läbi (eeldusel, et seal ei visatud uut erindit), siis programmi töö jätkub viimase catch
ploki järelt.
See, millises järjekorras on catch
plokid, on oluline. Kui catch
plokkide parameetrite hulgas on üksteisest päritud erindiklasse, siis tuleb alamklassi erindi töötlemine panna ülemklassi erindi töötlemisest ettepoole. Vastasel juhul ei jõuta kunagi alamklassi erindi töötlemiseni, sest püünise valikul sobib ka erindiklassi ülemklass (alamklassi objekt on alati ka ülemklassi objekt).
Juhul, kui kõiki eriolukordi ei osata ette näha, aga on vaja tekkiv erind siiski kinni püüda, siis võib püünisena kasutada ka catch
plokki kujul catch(...)
. See püünis peab olema viimane ja sellesse suunatakse juhtimine juhul, kui ükski eelnev püünis ei sobinud.
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> #include <string> 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 (const 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 (const 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 (const int ex) { cout << "Erindi töötlemine f2()-s\n"; } cout << "f2() lõpetab\n"; } void f3() { cout << "f3() alustab\n"; try { erindiviskaja(); cout << "f3()-s peale erindiviskamist\n"; } catch (const int ex) { cout << "Erindi töötlemine f3()-s\n"; } cout << "f3() lõpetab\n"; } | 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\n"; erindiviskaja(); cout << "f3()-s peale erindiviskamist\n"; cout << "f3() lõpetab\n"; }
Nüüd funktsioonis f3()
erindit ei töödelda, see delegeeritakse pöördujale, st funktsioonile f2()
. Programmi 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
Joonisel on nooltega näidatud programmi täitmise järjekord:
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'
Erindi uuesti viskamine (rethrow)
Mõnikord on vaja püünises (catch
-plokis) visata uuesti sama erind. Seda saab teha käsuga throw
, mille järele ei kirjuta midagi. Siis uut erindiobjekti ei tehta, vaid visatakse püüdmiseks uuesti seesama erind (erindiobjekt). 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 käsuga throw
.
#include <iostream> #include <string> using namespace std; // Näitab objektide loomist (konstruktor) ja hävitamist (destruktor) class Näita { public: // konstruktor Näita(const string& mis) : m_id{number}, m_mis{mis} { print("Näita konstruktor "); ++number; } // koopiakonstruktor Näita(const Näita& ex) : m_id{number}, m_mis{ex.m_mis} { print("Näita koopiakonstruktor "); ++number; } // destruktor ~Näita() { print("~ Näita destruktor"); } void print(const string& 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 (const 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 (const 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 sama erind uuesti. Funktsioonis loenda
on püünises erindi edastamine viite (reference) abil catch (const Näita& ex)
, aga funktsioonis main
koopia abil catch (const Näita ex)
, st toimub koopiakonstruktori poole pöördumine ja koopia hiljem hävitatakse. Esialgselt visatud erind kustutatakse alles main
funktsioonis. Kokku tehakse klassist Näita
kaks objekti.
Kui funktsioonis loenda
kasutada erindi edastamist koopia abil catch (const Näita ex)
, siis tehakse edastamiseks erindist iga kord koopia ja püünises see koopia hävitatakse. Järgmises näites tehakse klassist Näita
kokku viis objekti.
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 (reference).
Erindite standardteek
Teegis <exception>
on C++ erindite klassid, mida saab erindite töötlemisel kasutada. Kõik standardteegi erindid on päritud klassist exception
ja asuvad standardnimeruumis std
. Toome erindite klassi hierarhiast olulisema osa:
Erindid, mis on logic_error
tüüpi, visatakse vigade korral, mis avastatakse (vähemalt põhimõtteliselt) enne programmi täitmist, sest need on põhjustatud programmi loogikavigadest. Tüüpilised olukorrad, kus loogikavead visatakse, on näiteks funktsiooni väljakutsed ühe või mitme sobimatu argumendiga. Neid vigu saab oma programmis vältida, kontrollides argumentide sobivust enne funktsiooni väljakutset.
Teine rühm, mis on päritud erindist runtime_error
, on vigade jaoks, mis on andmetest sõltuvad ja mida saab tuvastada ainult programmi käitamisel. Siia alla kuuluvad ka system_error
-st päritud erindid, mille aluseks on pöördumised operatsioonisüsteemi poole, nt ebaõnnestunud faili sisend või väljund. Failidele juurdepääs, nagu ka igasugune suhtlus riistvaraga, võib alati ebaõnnestuda viisil, mida ei saa ette ennustada, nt kettarikked, võrgurikked jne.
Erindid bad_alloc
võivad tekkida operaatori new
kasutamisel, nt massiivi negatiivne elementide arv, mälutõrge jne.
Erind bad_cast
visatakse dynamic_cast<T&>(avaldis)
poolt, kui avaldist ei ole võimalik teisendada tüübiks T
.
Erind bad_typeid
visatakse operaatoritypeid
poolt, kui seda kasutatakse otsendatud nullviida korral (dereferenced null pointer).
Erindiklassi pärimine standardteegist
Oma erindiklassi saab pärida ka sisseehitatud klassist. Sellisel juhul tuleb kaasata päis exception
. Järgmises näites visatakse erind peale negatiivse arvu sisestamist. Programmi käitati kaks korda.
#include <exception> #include <iostream> #include <string> 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 (const MinuErind& e) { cout << "Püüti MinuErind: " << e.getSõnum() << ": " << e.getArv(); return 1; } return 0; } | Sisesta positiivne täisarv: 2 Sisesta positiivne täisarv: -5 Püüti MinuErind: Sisestasid negatiivse arvu!: -5 |
Kui soovime kasutajalt ikkagi positiivset arvu, siis paneme try-catch
plokid tsüklisse ja kordame kuni kasutaja sisestab positiivse arvu:
int main() { int x{}; while (true) { try { cout << "Sisesta positiivne täisarv:\n"; cin >> x; if (x < 0) { throw MinuErind("Sisestasid negatiivse arvu!", x); } cout << "Sisestasid: " << x; return 0; } catch (const MinuErind &e) { cout << "Püüti MinuErind: " << e.getSõnum() << ": " << e.getArv() << '\n'; } } return 0; } | Sisesta positiivne täisarv: -5 Püüti MinuErind: Sisestasid negatiivse arvu!: -5 Sisesta positiivne täisarv: 3 Sisestasid: 3 |
Võtmesõna 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
https://en.cppreference.com/w/cpp/language/exceptions
Klassimall optional
Alates C++17 on võimalik kasutada standardteegi klassimalli optional
, mis võimaldab eriolukordi mugavamalt käsitleda. Selleks tuleb kaasata päis <optional>
.
Sageli on olukordi, kus funktsioon tagastab normaalse lõpu korral tulemuse, kuid mingi eriolukorra tõttu (nt jagamine nulliga) ei ole võimalik sama tüüpi tulemust mõistlikult tagastada. Siin tuleb appi klassimall optional
.
Klassimall optional
esindab väärtuse olemasolu või puudumist tüübiohutul viisil. Vastava muutuja defineerime järgmiselt (T
on malli andmetüüp):
optional<T> muutuja;
Klassimallil on järgmised funktsioonid:
has_value
- kontrollib, kas objekt sisaldab väärtustvalue
- tagastab väärtusevalue_or
- tagastab sisalduva väärtuse, vastasel juhul teise väärtuse
Olgu meil funktsioon, mille tulemuseks on täisarvude jagatis
optional<int> jaga(int arv1, int arv2) { if (arv2 != 0) { return arv1/arv2; } return nullopt; // või ka return {}; }
Funktsioon tagastab optional<int>
objekti, mis sisaldab jagatist (täisarvu), kui arv2
ei ole null. Kui arv2
on null, siis tagastab funktsioon nullopt
, mis esindab väärtuse puudumist. Funktsioonis main
saab väärtuse olemasolu kontrollida funktsiooniga has_value
ja väärtuse saab objektist kätte funktsiooniga value
:
int main() { auto tulemus = jaga(10, 3); if (tulemus.has_value()) { // või if (tulemus) cout << "Tulemus: " << tulemus.value() << '\n'; } else { cout << "Jagamine nulliga." << '\n'; } return 0; }
Programmi töö tulemuseks on
Tulemus: 3
Funktsioon value_or
tagastab väärtuse korral muutuja väärtuse, vastasel juhul teise väärtuse.
optional<string> s; cout << s.value_or("kass") << '\n'; optional<string> s1{"koer"}; cout << s1.value_or("teine koer") << '\n'; | kass koer |
Klassimalli optional
kasutamine muudab koodi loetavamaks ja vähendab väärtuse puudumise ja null-viitamise vigu.
Enesetestid
NB! Enesetestides eeldame, et on kasutatud standardnimeruumi (using namespace std;
)