Kirjautuminen

Haku

Tehtävät

Koodit: C++: Operaattorien ylikuormitus; 3D-vektoriluokka

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;
}

Kommentit

L2-K2 [02.01.2008 13:25:39]

#

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

kayttaja-2791 [02.01.2008 14:06:33]

#

Hyvä vinkki, tarpeeksi yksinkertainen että meikäläinenkin ymmärtää sisällön vaikka C++:aan en vielä ole kajonnutkaan.

Pekka Karjalainen [02.01.2008 15:20:41]

#

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.

ByteMan [02.01.2008 22:04:08]

#

aika siisti..
hyvä, selkeä, ilmeisesti myös hyödyllinen jos joskus 3d-ohjelmointiin asti pääsen..

Metabolix [03.01.2008 15:58:56]

#

Hyviä huomioita. Lisäsin nollallajaon estämiseksi throw-lauseen ja laitoin skalaarikolmitulonkin esimerkkeihin.

juzzlin [23.02.2010 11:04:21]

#

!-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
}

Metabolix [23.02.2010 11:43:48]

#

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.

juzzlin [23.02.2010 14:35:41]

#

Totta, mutta tuo nyt vain oli esimerkki siitä kuinka asian voi tehdä :) Yleensä paras ratkaisu (ja tarkkuus) on sovelluskohtainen.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta