Kirjoittaja: Metabolix
Kirjoitettu: 02.01.2008 – 02.01.2008
Tagit: koodi näytille, vinkki
C++:n luokat tarjoavat kätevän mahdollisuuden antaa uusia merkityksiä kielen normaaleille operaattoreille kuten "+", "-", "*" ja "/". Tällöin vektorit, matriisit ja monet muut matemaattiset oliot on mahdollista toteuttaa niin, että niitä voi käyttää laskuissa tuttuun tapaan matemaattisilla merkinnöillä ilman sekavia funktiokutsuja. Lopputulos on tästä huolimatta aivan sama kuin funktiototeutuksessa, koska operaattoritkin ovat vain funktioita: "a + b" on sama asia kuin "+(a, b)", C++-kielellä "operator + (a, b)
". Operaattori on siis vain nimeltään ja merkintätavaltaan erikoinen funktio.
Aina on syytä muistaa, ettei kääntäjä ymmärrä ohjelmoijan ajatuksia. Kuitenkin C++-kääntäjään on ohjelmoitu tietynlaista älyä. Se nimittäin osaa valita useasta samannimisestä funktiosta sen, joka parhaiten vastaa funktiolle annettuja arvoja. Hyvä esimerkki on se, kuinka C++:n standardivirtoihin tulostetaan: sama operaattori "<<" toimii yhtä lailla erilaisten lukumuuttujien kuin tekstinkin tulostamiseen — puhumattakaan siitä, että myös tulosteen muotoilu tapahtuu saman operaattorin välityksellä.
Tämä funktion valinta muuttujatyypin mukaan tarkoittaa käytännössä myös sitä, että vaikkapa vektoriluokalle on mahdollista määritellä samalla kertolaskuoperaattorilla sekä luvulla että toisella vektorilla kertominen. Kääntäjä tietää koodista, kumpaa funktiota tarvitaan. Sen sijaan pistetulolle ja ristitulolle täytyy olla eri operaattorit, koska kummassakin tapauksessa kerrottavat ovat molemmat vektoreita eikä kääntäjä siis näe mistään, kumpi toimitus on kyseessä.
Seuraavassa koodissa kuvataan yksinkertainen luokka 3D-vektoreille. Luokka sisältää kaikki yleisimmät toimitukset: summan, erotuksen, kerto- ja jakoskaalaukset, pistetulon ja ristitulon sekä funktioiden muodossa vielä pituuden ja normalisoinnin. Lopussa on vielä varomattomille vaarallinen tyyppimuunnos vektorista luvuksi (pituudeksi). Luokka ei tietenkään ole täydellinen, koska tarkoituksena on vain demonstroida operaattoreita.
// Neliöjuurta varten matematiikkaotsikko #include <cmath> // Toiseen korotusta varten funktio template <typename T> inline T pow2(T x) { return x * x; } // Vektoriluokka class vektori { // Itse vektori: v = xi + yj + zk double x, y, z; public: // Muodostimet: nollavektori tai määrätty vektori vektori() : x(0), y(0), z(0) {} vektori(double _x, double _y, double _z) : x(_x), y(_y), z(_z) {} // Vastavektori; const tarkoittaa, ettei alkuperäisen vektorin arvo muutu vektori operator - () const { // Palautetaan uusi vektori, jonka komponentit ovat vastakkaiset return vektori(-x, -y, -z); } // Peruslaskutoimitukset vektori operator + (vektori const& t) const { return vektori(x + t.x, y + t.y, z + t.z); } vektori operator - (vektori const& t) const { return vektori(x - t.x, y - t.y, z - t.z); } // Kertominen ja jakaminen luvulla vektori operator * (double t) const { return vektori(x * t, y * t, z * t); } vektori operator / (double t) const throw (char const*) { if (!t) throw "Jaoit nollalla!"; return vektori(x / t, y / t, z / t); } // Pistetulo; // Sama * kelpaa kuin luvun kanssa kerrottaessa. // Kääntäjä valitsee oikean vaihtoehdon sen mukaan, // onko oikealla luku vai toinen vektori. double operator * (vektori const& t) const { return x * t.x + y * t.y + z * t.z; } // Ristitulo; // Käytetään %:ta, kun se on kätevästi vapaana. vektori operator % (vektori const& t) const { return vektori( y * t.z - z * t.y, z * t.x - x * t.z, x * t.y - y * t.x ); } // Funktio vektorin pituutta varten double len_sqr() const { // Pistetulo v * v on vektorin v pituuden neliö return (*this) * (*this); } double len() const { // Pituus on pituuden neliön neliöjuuri. return std::sqrt(len_sqr()); } // Normalisoitu eli yhden yksikön pituinen vektori vektori norm() const { return *this / len(); } // Vektorin muunto luvuksi palauttakoon pituuden. // Tarkkana siis, ettei tule virheellisiä sijoituksia! operator double () const { return len(); } }; // Jäsenfunktioihin sisältyy vasta "vektori * double" // Toisin päin operaattori täytyy määritellä ulkopuolella vektori operator * (double luku, vektori const& v) { // Turha sitä on uudestaan naputella, kun vaihdantalaki pätee return v * luku; } // Jakolaskua toisin päin ei olekaan.
int main(void) { // Oletusmuodostin nollaa kaiken vektori nollavektori; // Vektorin voi myös määritellä tässä vektori a(1,1,1), b(1,2,3); // Muodostin c(a) luo c:n kopioimalla a:sta. vektori c(a); double d = 10; c = -a; // Vastavektori c = a + b; // Vektorien summa c = a - b; // Vektorien erotus d = a * b; // (vektori * vektori) eli jäsenfunktion pistetulo-operaattori c = a * d; // (vektori * luku) eli jäsenfunktion kertolaskuoperaattori c = d * a; // (luku * vektori) eli erillinen kertolaskuoperaattori c = d * a; // Kertominen luvulla toisin päin c = a / d; // Jakaminen luvulla c = a % b; // Ristitulo d = a; // Pituus operaattorin avulla // Mutkikkaammatkin lausekkeet onnistuvat: // Skalaarikolmitulo d = a % b * c; d = a * (b % c); // Näin päin sulut tarpeen! // Pistettä avaruudessa on hyvä kuvata paikkavektorilla. // Pisteen c etäisyys suorasta ab optimoituna: c = vektori(2, 0, 0); b = vektori(); a = vektori(5, 5, 0); d = sqrt((c - a).len_sqr() - pow2((b - a) * (c - a)) / (b - a).len_sqr()); return 0; }
Hieno luokka...
Muuten skaalarikolmiotulo ei taida toimia "oikein" tuossa, ristitulo tulee laskea ennen pistetuloa.
//a, b, c vektoreita a%b*c = a*b%c // * pistetulo, % ristitulo //luokka vaatii a%b*c = a*(b%c) jotta toimisi
Hyvä vinkki, tarpeeksi yksinkertainen että meikäläinenkin ymmärtää sisällön vaikka C++:aan en vielä ole kajonnutkaan.
Pieni löyntivirhe sattunut toisessa kappaleessa.
ohjemoitu > ohjelmoitu
Nollavektorin normalisointi ei tunnetusti onnistu. Tässä toteutuksessa se aiheuttaa nollalla jaon norm-metodissa (tai oikeastaan /-operaattorimetodissa), mistä sopisi ehkä lyhyt maininta kommentissa. Tätähän ei välttämättä tarvitse käsitellä mitenkään erikoisesti, mutta luokan käyttäjiä siitä olisi kuitenkin hyvä muistuttaa.
aika siisti..
hyvä, selkeä, ilmeisesti myös hyödyllinen jos joskus 3d-ohjelmointiin asti pääsen..
Hyviä huomioita. Lisäsin nollallajaon estämiseksi throw-lauseen ja laitoin skalaarikolmitulonkin esimerkkeihin.
!-operaattoria ei voi käyttää luotettavasti testaamaan, onko liukulukumuuttuja "nolla". Luotettavampi tapa voisi olla esim. näin:
#include <limits> if (abs(t) <= std::numeric_limits<double>::epsilon()) { // Nolla }
juzzlin: Siinä olet oikeassa, että pyöristysvirheiden vuoksi liukulukulaskujen tulokset voivat olla vähän niin ja näin. Ratkaisusi on kuitenkin väärä.
Jos tehdään laskelmia nanometrien kokolukassa, aika monesta asiasta tulee sinun logiikallasi "nolla". Myös esimerkiksi alkeisvaraus olisi jo sellaisenaan "nolla", samoin protonin massa. Eipä tule fyysikoille kovin järkeviä tuloksia.
Tarkistuksen ajatus sinänsä on oikeilla jäljillä, mutta koska liukuluvun pienin normalisoitu arvo on 2.225074e-308 ja denormalisoitu 4.940656e-324, tarkistus suoraan epsilonia (2.220446e-16) vastaan ei ole mielekäs. Jonkinlainen skaalaus olisi tarpeen.
Totta, mutta tuo nyt vain oli esimerkki siitä kuinka asian voi tehdä :) Yleensä paras ratkaisu (ja tarkkuus) on sovelluskohtainen.