Lueskelin tuota C++-opasta muistin virkistämiseksi ja sieltä löytyikin uutta tietoa. Harmikseni tämä uusi tieto oli niin vaikeasti kirjoitettu, että en ymmärtänyt siitä mitään. Annettu koodikaan ei edes toiminut, niin ei siitä oikein mitään saanut irti.
Mihin noita toimintoja siis käytännössä tarvii? Kokeilin oma-aloitteisesti tehdä seuraavaa koodia (joka yllätyksekseni toimi!):
#include <iostream> using namespace std; int main() { try { throw 2.0; } catch (int virhe) { cout << "Virhekoodi (int): " << virhe << endl; } catch (double virhe) { cout << "Virhekoodi (double): " << virhe << endl; } }
Tajuntaani vain ei mahdu, että mikä hyöty tuon catch-lauseen tekemisessä oli? Nythän siis ohjelma kaatuu, jos throwaa vaikka doublen (2.0f). Muistan kun joskus koulussa oli Java-kurssi, niin tuolla try/catch -systeemillä kierrettiin mahdolliset virheet.
Laitetaan tähän samaan kerään nyt vielä kysymys, että mitä ja mistä kannattaisi seuraavaksi opiskella, kun osaan C++:n perusteet ja olio-ohjelmoinnista myös. Tavoitteena olisi jonkun 3D-pelin ohjelmointi jossain vaiheessa, mutta ensin kannattaa varmaan yrittää jotain 2D:tä tehdä esim. SDL:llä? Vai mitä olette mieltä?
Virheiden löytämiseen ja käsittelyynhän tuo tosiaan on. Kun löydetään virhe try-lohkossa, heitetään throwilla tieto mistä on kyse tai mikä vian aiheutti. Catch sitten tutkii mitä annettiin ja sen mukaan käsittelee virheen oikeassa lohkossa. Kätevää tässä on se, että se pomppaa sinne catch-lohkoon jopa funktiokutsun aikana.
Mikä idea on sitten noilla eri tyyppien catch-kohdilla? Eikös niitä virheitä voisi ihan hyvin pistää kaikki vaikka int-muuttujalla. 1 = Eka virhe, 2 = Toka virhe jne.
Niin, ja millä tuo virhe löydetään. Tarkoitetaanko tällä siis if-lauseella testaamista. Tällä ei vissiin löydä esim. käyttäjän syöttämiä liian pitkiä merkkijonoja tai vastaavia?
Voihan sillä jäsentää eri virhetapauksia kätevästi:
try { // ... } catch (ParseError *e) { // ... } catch (MemoryAccess *e) { // ... }
If-lauseella voi tarkastaa onko virhettä.
if ((Matti && Mervi) == Yhdessa) { throw IltaSanomat.UusiLooppi(); }
Grandi kirjoitti:
try { // ... } catch (ParseError *e) { // ... } catch (MemoryAccess *e) { // ... }
Kaappaat osoittimia. Saako tuota muka toimimaan ilman muistuvuotoja?
Minulle ainakin on opetettu, että poikkeukset kannattaa kaapata viitteillä
try { // ... koodia, jossa voi sattua esim: throw ParseError("virheviesti tms"); // ... throw voi myös toki olla myös jossain tämän koodin kutsumassa funktiossa } catch (ParseError& e) { // ... vaikka: std::cerr << "Kävi huonosti: " << e.getMsg() << std::endl; } catch (MemoryAccess& e) { // ... }
Minulle on myös opetettu, että throw:n käyttäminen minkään muun kuin virhetiedon välittämiseen on rumaa ja yleensä kertoo koodin huonosta suunnittelusta.
punppis kirjoitti:
Mikä idea on sitten noilla eri tyyppien catch-kohdilla? Eikös niitä virheitä voisi ihan hyvin pistää kaikki vaikka int-muuttujalla. 1 = Eka virhe, 2 = Toka virhe jne.
Erityyppiset virheet on kätevää esittää eri luokilla ja käsitellä eri catcheissä. Erityyppisiin virheisiin voi liittyä erilaista lisätietoa. Esim. tiedoston lukemisessa sattuneeseen virheeseen voi olla hyödyllistä liittää rivinumero.
On usein hyvä antaa virheitä esittävien luokkien periä sama yläluokka, jolloin tarvittaessa voidaan kaapata kaikki virheet yläluokan tyyppisellä catch
:llä. Jotkut tykkäävät käyttää yhteisenä yläluokkana standardikirjaston std::exception
ia. (catch (...)
kaappaa myös ihan kaiken, mutta siitä ei oikein saa mitään lisätietoa irti.)
punppis kirjoitti:
Niin, ja millä tuo virhe löydetään. Tarkoitetaanko tällä siis if-lauseella testaamista. Tällä ei vissiin löydä esim. käyttäjän syöttämiä liian pitkiä merkkijonoja tai vastaavia?
Kyllä, omat virhetilanteet pitää testata vaikka if-lauseella. Throw on vain kätevä tapa välittää tieto virheestä kutsujalle.
Varsinainen asiasisältö try-rakenteesta tähän mennessä on opassarjan kolmannessa osassa, erityisesti esimerkkiohjelman toiminnassa ja kommenteissa, joita et ilmeisesti ole tutkinut tarpeeksi. Siellä esimerkiksi heitetään virheilmoituksia sen mukaan, millainen virhe on tapahtunut.
punppis kirjoitti:
Annettu koodikaan ei edes toiminut, niin ei siitä oikein mitään saanut irti.
Puhut ilmeisesti funktio-oppaan esimerkistä. Annettu koodi toimii aivan oikein, ja jos paneutuisit hieman siihen, varmasti voisit ymmärtääkin sen. Katso ohjelman tulostusta, niin näet, missä järjestyksessä rivit ajetaan ja minne poikkeukset lentävät. Koodin on tarkoituskin demonstroida, että vääränlaisen poikkeuksen heittäminen kaataa ohjelman. Tätä ennen heitetään kuitenkin monta oikeaa poikkeusta, joiden kulkua voi seurata tulosterivien perusteella.
punppis kirjoitti:
Mikä idea on sitten noilla eri tyyppien catch-kohdilla?
Se, että voi heittää ja ottaa kiinni erilaisia poikkeuksia. Tietääkseni kukaan ei ole väittänyt, että niitä olisi pakko olla useampi, joten pelkkien lukujen heittäminen toimii oikein hyvin. Opassarjan tulevissa osissa kerrotaan lisää virheenkäsittelystä standardikirjaston poikkeusluokkien ja niistä periytettyjen omien luokkien avulla, ja siinä vaiheessa tarkoitus ehkä selviää paremmin.
punppis kirjoitti:
jos throwaa vaikka doublen (2.0f).
Opassarjan toisesta osasta voit lukea, että 2.0f on nimenomaan float eikä double, ja ohjelmasi kaatuu siihen, koska sinulla ei ole float-tyyppiselle poikkeukselle käsittelijää.
punppis kirjoitti:
Muistan kun joskus koulussa oli Java-kurssi, niin tuolla try/catch -systeemillä kierrettiin mahdolliset virheet.
Tarkoitushan ei ole ensisijaisesti "kiertää" virheitä, vaikka Javassa ehkä tuntuukin siltä. Javassa poikkeukset viestivät kaikenlaisista standardikirjaston virheistä, ja tietenkin poikkeuksiin täytyy reagoida jotenkin järkevästi, usein ilmoittamalla ongelmasta käyttäjälle tai yrittämällä uudestaan.
Sikäli tilanne Javan ja C++:n välillä on sama, että kummassakin tapauksessa ohjelma kaatuu, jos heitettyä poikkeusta ei oteta kiinni. C++:n standardikirjasto (jota on tähän mennessä käsitelty lähinnä tulostus- ja lukukomentojen verran) ei kuitenkaan juuri heitä poikkeuksia vaan palauttaa funktiokutsuista arvoja tai asettaa sisäisiä muuttujia kertomaan virheestä. Näitä voi (ja pitää) tarkistaa itse, kuten try-esimerkissä kohdassa std::cin.good()
tehdään.
map_ kirjoitti:
Minulle on myös opetettu, että throw:n käyttäminen minkään muun kuin virhetiedon välittämiseen on rumaa ja yleensä kertoo koodin huonosta suunnittelusta.
Tämä on aivan totta, ja mainitsen kyllä asiasta oppaassakin. Esimerkit ovat kuitenkin huonoja, koska tarkoitus on esitellä rakenteen toimintaa mieluiten välineillä, jotka ovat jo lukijalle tuttuja. Kuten jo mainitsin, varsinaiset virheenkäsittelyosat ilmestyvät myöhemmin, kunhan päästään kunnolla luokkiin ja standardikirjastoon asti.
Kiitos kummallekin palautteesta, yritin hieman selkeyttää mainittuja kohtia oppaista.
Poikkeusten ideana on, että jos jossain funktiossa tapahtuu virhe, siitä toipuminen voi edellyttää funktion kutsujaa muuttamaan toimintaansa.
Esimerkiksi C-kielessä, josta poikkeukset puuttuvat, tämä hoidetaan usein siten, että funktiot palauttavat arvon, joka kertoo, onnistuiko operaation suorittaminen vai ei. Tämän seurauksena on kuitenkin helposti rumaa koodia, kun joudutaan huolehtimaan monien funktioiden mahdollisista virheistä:
int tee_monta_asiaa() { int tulos; tulos = eka_funktio(); if (tulos != OK) goto hoida_virhe; tulos = toka_funktio(); if (tulos != OK) goto hoida_virhe; tulos = kolmas_funktio(); if (tulos != OK) goto hoida_virhe; /* jne... */ return OK; hoida_virhe: printf("Tapahtui virhe: %s\n", hae_virheteksti(tulos)); return tulos; /* Palautetaan sama virhe kutsujalle. */ }
Jos funktiot virhekoodin palauttamisen sijaan heittävätkin virhetilanteessa poikkeuksen, päästään lyhyemmällä ja selkeämmällä koodilla, sillä virheistä huolehtiva koodi on nyt erillään normaalista toiminnasta huolehtivasta koodista:
void tee_monta_asiaa() { try { eka_funktio(); toka_funktio(); kolmas_funktio(); // ... } catch(poikkeus& p) { std::cout << "Tapahtui virhe: " << p.what() << std::endl; throw; // Heitetään sama poikkeus eteenpäin. } }
Joo nyt selvensi jo paljon, kun tajusin lukea tuon opassarjan kolmannen osan. Skippasin sen kokonaan sillä luulin että siinä käsitellään vain ehtoja ja silmukoita (kuten otsikossakin sanotaan), sillä ne on ihan hyvin hallussa.
Niin ja vielä tuohon Metabolixin tekstiseinään vielä, että piti kirjoittaa kyllä jos heittää _floatin_ 2.0f, mutta jostain sinne pääsi silti se "double".
Aihe on jo aika vanha, joten et voi enää vastata siihen.