Jokseenkin filosofinen kysymys: Pitäisikö esim. kuormitetun jakolaskuoperaattorin tarkistaa, ettei jakaja ole nolla (ja vaikka heittää poikkeus)? Vai pitäisikö sen jättää vastuu tästä käyttäjälleen?
Pyritäänkö operaattorien kuormituksessa mahdollisimman lyhyisiin ja yksinkertaisiin toteutuksiin, vai olisiko suotavaa/oikeellista tehdä operaattorin kuormituksen toteutuksesta pitkä ja mutkikas? Voiko se olla tarpeellista jossakin tilanteessa, ja jos niin millaisessa?
Lyhyesti: pitäisikö operaattorin kuormitukseen suhtautua kuten mihin tahansa muuhunkin funktioon?
En pyri näillä kysymyksillä paljon mihinkään, yritän vain laajentaa käsitystäni kirjoittamattomista säännöistä :P
Kiitän!
Operaattorin pitää toimia loogisesti, ja toiminnan voi dokumentoida. Sopiva ratkaisu riippuu tarkoituksesta.
Tavallaan filosofinen kysymyksesi pelkistyy siihen, onko luokan tarkoitus luoda omat sääntönsä johonkin asiaan (enemmän tarkistuksia) vai vain säilyttää data ja lyhentää joitakin yleisiä merkintöjä (ei tarkistuksia). Matematiikassa suosisin yleensä jälkimmäistä tapaa, koska matematiikassa yleensäkin pitää itse tietää asiansa. Monessa muussa tilanteessa tykkään tarkistella, jotta mahdolliset (vaikka epätodennäköisetkin) virhetilanteet ovat edes jotenkin hallinnassa.
Jos on tarkoitus tehdä vaikka float-tyyppiä vastaava kompleksiluku, voi olla perusteltua jättää tarkistukset tekemättä, jolloin virheellinen operaatio käyttäytyy samalla tavalla kuin tavallisen float-muuttujan kanssa. (Liukuluvuilla tosin nollallakin voi jakaa, kun taas kokonaisluvuilla tulee kunnon virhe.) Hankalampi rajatapaus on, mitä pitäisi tehdä, jos vaikka kompleksiluvun toinen komponentti ylittää lukualueen ja muuttuu äärettömäksi mutta toinen ei; jos tästä on huolta, ehkä tarkistukset ovat tarpeen.
Jos luokka on alkujaan mutkikas eikä operaattorilla ole selvää yhteyttä taustalla oleviin perustietotyyppeihin (tai luokan sisältämät arvot ovat yksityisiä eikä kutsujan voi edellyttää tarkistavan niitä), on usein perusteltua tehdä enemmän tarkistuksia myös operaattorin kohdalla. Eräs kompromissi on tehdä ylimääräiset tarkistukset esimerkiksi assert-makrolla tai vastaavalla niin, että ne saa helposti esikääntäjän asetuksilla poistettua ohjelman lopullisesta versiosta.
Kiitos kattavasta vastauksesta.
Liukuluvuilla laskettaessa nollalla jakaminen tuottaa tulokseksi luvun ääretön tai miinus ääretön. Nämä ovat mahdollisia arvoja liukulukutyypille. Joskus nämä arvot voivat tulostua esimerkiksi muodossa Infinity ja -Infinity. Näin käy ainakin Haskell-kielen toteutuksessa, joka minulla on tässä auki.
Jos taas suorittaa laskutoimituksen, jonka arvo ei voi olla edes ääretön millään järkevällä tavalla, tuloksen on erityinen luku, joka ei ole luku. Tämän arvon nimi on NaN (epäluku), joka tulee sanoista Not a Number. Kaikki laskutoimitukset NaN-arvolla tuottavat uuden NaN-arvon. Se on kuin kuoppa, jonne kerran pudottua ei pääse pois.
Lisäksi NaN ei ole yhtä suuri kuin itsensä. Esim. seuraavan pseudokoodin mukaisen koodin pitäisi tulostaa false kaikissa liukulukustandardin mukaista toteutusta käyttävissä ympäristöissä.
var x = 0/0 ; antaa tulokseksi NaN print(x == x) ; testin tulos on epätosi!
Harjoitus: Kirjoita funktio isNaN, joka tutkii onko sen parametri NaN ja palauttaa tosi vain kun se on.
Miksi liukuluvut on toteutettu näin? Idea on se, että jos laskennassa kerran tapahtuu virhe, sen tuloksena on arvo, jolla voi yhä laskea. Joissakin sovelluksissa NaN-arvot jätetään laskuista pois tietyissä laskutoimituksissa (vaikkapa lukujoukon keskiarvon laskemisessa, joissa NaN voidaan tulkita puuttuvaksi arvoksi), ja joskus tietyt laskutoimituksen voivat aiheuttaa poikkeuksen tai muun virheen, kun niitä sovelletaan äärettömään tai NaN-arvoon. Yksittäisillä arvoilla laskiessa NaN-arvo voi propagoitua eli edetä koko laskutoimituksen läpi, ja laskujen voi huomata menneen pieleen vasta kun tulosta tutkii.
Wikipedia ja muut lähteet kertovat enemmän detaljeista ja miten liukuluvuissa esitetään nämä erityiset arvot, sekä ne normaalit äärelliset arvot.
Pointti on se, että liukuluvuissa on ominaisuus virheenkäsittelylle mukana. Kokonaisluvuissa ei ole sellaista C++-kielen tasolla, vaan niissä ylivuoto joko tarkoittaa luvun kiertämistä ympäri isosta arvosta nollaan (jos tyyppi on unsigned) tai signed-tapauksessa, mikäli nyt oikein muistan, määrittelemätöntä tulosta, joka siis riippuu kyseisestä kääntäjästä ja tietokonearkkitehtuurista.
Nollalla jako kokonaisluvuilla on taas virhe, joka ei tuota kokonaislukua ollenkaan, vaan jonkinlaisen poikkeuksen. Omalla Linux-koneella 1/0 C++-ohjelmassa tuotti ensin kääntäjän varoituksen (hyvä kääntäjä!) ja sitten Floating point exception -ilmoituksen. Joka ei ihan kirjaimellisesti ole totta, koska kyseessä olivat kokonaisluvut, mutta kelpaa meille. Olisi ollut ikävä saada vaikkapa tulos nolla, koska se ei ole aritmeettisesti oikea tulos, mutta näyttää siltä.
Jos haluat oman tietotyyppisi käyttäytyvän kuin kokonaisluvut (esim. jos se perustuu niihin), on nollalla jaon tarkistus ja poikkeuksen heittäminen mahdollinen idea. Liukulukuja muistuttavassa tilanteessa, jossa vaikkapa ovat nämä äärettömyydet ja epäluvut tarjolla, voi tarkistuksen jättäminen pois olla perusteltua. Ajatus on, että koodisi käyttäjä on niin viisas, että osaa itse testata äärettömyyksiä ja epälukuja oikein.
Virheen testaukset on toki helpompi poistaa turhina ohjelmasta jossa niitä on kuin tapella virheiden kanssa, jotka ne olisivat alunperin estäneet. Liikaa tarkistuksia on siis pienempi paha kuin liian vähän tällä logiikalla.
http://en.wikipedia.org/wiki/IEEE_floating-point_standard
http://en.wikipedia.org/wiki/NaN
http://en.wikipedia.org/wiki/Division_by_zero#In_computer_arithmetic
Olisi kiva, jos Putkassa olisi kunnon opas näistä asioista ja vinkki assertin oikeasta käytöstä. Jatkossa voisi sitten viitata niihin, kun pitäisi kirjoittaa tällainen kattava vastaus ja asiat tulisivat paremmin selville. En tarjoudu kuitenkaan sellaista kirjoittamaan nyt tänä iltana. Tässäkin esityksessä on puutteita, ellei jopa virheitä. Ne korjaavat ihmiset voivat sitten viitata luotettaviin lähteisiin, jos haluavat. Kiitos korjauksista jo etukäteen.
Tarkoitus ei ollut kritisoida Metabolixia. Minusta olit herra M ihan oikeassa vastauksessasi, mutta arvelin, että tämä oleellinen osa asiaasi jäi mystiikaksi ainakin pienelle osalle putkalaisia, jotka eivät tiedä IEEE-standardista vielä mitään.
Pekka Karjalainen kirjoitti:
var x = 0/0 ; antaa tulokseksi NaN
Eikö tuo koodi tavallisimmilla kielillä kaada ohjelman kumoon kuin korttipakan? :)
User137 kirjoitti:
Pekka Karjalainen kirjoitti:
var x = 0/0 ; antaa tulokseksi NaNEikö tuo koodi tavallisimmilla kielillä kaada ohjelman kumoon kuin korttipakan? :)
Tuollaisen koodin ei pitäisi edes suostua kääntymään millään kunnollisella kääntäjällä ohjelmointikielestä riippumatta.
Esimerkiksi PL/I-kääntäjä antaa virheen:
: IBM1948I S ZERODIVIDE condition with ONCODE=320 raised while evaluating restricted expression.
Jos taas kyseessä ei olisi tuon tyyppinen ns. 'restricted expression', nappaisi ON UNIT ajon aikaisen virheen antaen nollalla nollaa jakaessa ensin ZERODIVIDE:n ja tuohon heti perään INVALIDOP:n.
User137 ja jalski, kyllä koodi toimii hyvinkin monella kielellä, ja mainitun liukulukustandardin mukaan sen kuuluukin toimia. Toki monissa kielissä pitää kirjoittaa 0.0 / 0.0, jotta luvut olisivat liukulukuja. Joskus on myös eroa siinä, tehdäänkö lasku vakioilla vai muuttujilla: esimerkiksi Free Pascal -kääntäjä muuttaa jo käännösvaiheessa lausekkeen 0/0 arvoksi NaN, vaikka suoritusvaiheessa vastaava lasku muuttujilla aiheuttaa yleensä virheen.
Liukulukujen virheasetuksia voi joissain kielissä myös itse säätää mm. funktioilla SetExceptionMask (Pascalissa) ja feholdexcept (C:ssä).
Eiköhän palata keskustelun alkuperäiseen aiheeseen eli C++:n operaattoreihin, jos on vielä jotain lisättävää.
Aihe on jo aika vanha, joten et voi enää vastata siihen.