Mallit ja geneerinen ohjelmointi
Tee-se-itse: linkitetty lista
K�yn t�ss� l�pi k�yt�nn�nl�heisen esimerkin, jonka avulla toivottavasti ymm�rr�t mik� juju mallien takana piilee. Kuvittele, ett� sinun pit�isi ohjelmoida ammuskelupeli. Unohtaen kaiken muun mit� r�iskint�pelin ohjelmointiin voisikaan liitty�, keskitymme yhteen haasteeseen: miten hoitaa ammuttujen laukausten k�sittely (siis niiden jotka lentelev�t ymp�riins�). Selv�stikin laukausta esitt�v�t oliot pit�� ker�t� yhteen, jotta niiden sijaintia voi p�ivitt�� lentoradan mukaan ja mahdolliset osumat havaita. Yht�aikaa voidaan joutua k�sittelem��n suuriakin m��ri� ammuksia (asearsenaali on hyvin konetulipainotteinen). Joten jos ker��mme ammutut kutit taulukkoon, tulee sen olla suuri.
Suurimman osan ajasta ei lent�vi� luoteja ole yht��n, koska pelaaja lymyilee vihollisten n�kym�tt�miss�. Eli suurimman osan ajasta taulukko veisi turhaan suuren m��r�n muistia ja teki siit� kuinka ison tahansa, aina voi tulla eteen tilanteita jossa se ei ole riitt�v�n iso. Tarvitsemme fiksumpaa tietorakennetta, linkitetty� listaa. Lista idea on, ett� yhdest� tietoalkiosta on aina osoitin seuraavaan - siis sen kokoa ei ole rajoitettu ja alkioiden lis��minen ja poistaminen on helppoa.
Luonnostelemme Ammus-luokan:
class Ammus { public: // saantifunktiot private: int x; int y; int z; Ammus* seuraava; };Koska Ammus-luokka mallintaa pelin todellisuuden k�sitett�, sen pit�isi olla mahdollisimman yhdenmukainen peliss� suihkivien ammusten kanssa. Ja se onkin, ammuksella on sijainti jota kolmella koordinaatilla (x, y, z) kuvataan.
Sopiiko Ammus*-osoitin kuvaan? Ei, koska pelin todellisuudessa ammuksia ole mitenk��n linkitetty toisiinsa. Ratkaisu ei ole siis paras mahdollinen: samaa linkitetty� listaa olisi mukava k�ytt�� muuallakin ja toisaalta mik�li ammuksien tallentamiseen p��tet��n k�ytt�� jotain viel� fiksumpaa tietorakennetta, on kurjaa menn� muuttamaan Ammus-luokkaa joka ei tietorakenneasiaan suuremmin liity. Siis linkitetyn listan toteutus pit�� repi� irti Ammus-luokasta (toki listalla olisi my�s oma luokkansa, jonka kautta sit� k�ytett�isiin; t�ss� on esitelty pelk�st��n yht� linkki� eli solmua esitt�v� luokka jotta esimerkit olisivat lyhyit� ja ytimekk�it�):
class ListanSolmu { public: // saantifunktiot private: Ammus* alkio; ListanSolmu* seuraava; };T�m� on parempi: linkitetyn listan voi vaihtaa ilman ett� Ammus-luokkaan tarvitsee koskeakaan. Mutta listaa ei voi viel�k��n k�ytt�� muiden luokkien kanssa, koska sen osoitin tietoalkioon on Ammus*-tyyppinen. C++-k��nt�j� huolehtii tietotyyppien oikeasta k�yt�st� - mik� toki on hyvin jalo ja kunnioitettava teko - eik� anna sijoittaa Ammus*-osoittimeen mink��n muun tyyppist� tietoa.
Tarvitsisimme osoitinta, joka kattaa kaikki mahdolliset tietotyypit. Ja sellainen onkin, nimitt�in void*. Mutta koska void* on tyypit�n osoitin, ei sen k�ytt� anna k��nt�j�lle mink��nlaista vinkki� siit� mit� me olemme oikein tekem�ss�, joten k��nt�j� ei my�sk��n osaa varoittaa jos teemme huolimattomuuserheen. Siis void*-osoittimella toteutettuun listaan voimme ammuksien sekaan sijoitella ep�huomiossa vaikka ensiapulaukkuja tai muuta ammuskelupeleiss� yleist� tarveaineistoa - k��nt�j� ei �r�hd� ollenkaan. Toisaalta olisihan se voitto maailman rauhalle, jos konekiv��ri ampuisikin vain laastereita ja sideharsoa...
void*-osoittimet eiv�t ole hyv� idea. Itseasiassa ajatus tyypitt�mien osoittimien k�yt�st� n�in korkean tason ohjelmoinnissa on loukkaus ket� tahansa osaavaa ohjelmoijaa kohtaan. Seuraavaksi turvaudummekin viisaisiin opetuksiin, joita t�ss� oppaassa tarjotaan. K�yt�mme abstraktiota, eli nousemme yl�s ansakuopasta siirt�m�ll� t�m�n pohdinnan korkeammalle abstraktiotasolle. Meh�n haluamme listan, joka osaa k�sitell� kaikkia pelin luokkia. Joten teemme luokan PeliObjekti, joka kuvaa mit� tahansa peliss� esiintyv�� asiaa ja josta kaikki pelin luokan on periytetty. Nyt - kiitos polymorfismin - voimme tehd� listan, joka PeliObjekti-kantaluokan avulla voi k�sitell� kaikkia pelin luokkia.
class PeliObjekti {};class Ammus : public PeliObjekti { public: // saantifunktiot private: int x; int y; int z; };class ListanSolmu { public: // saantifunktiot private: PeliObjekti* alkio; ListanSolmu* seuraava; };T�m� ei olekaan ihan huono ratkaisu, n�in nimitt�in Java-ohjelmointikieless� on toteutettu kielen standardikirjaston linkitetty lista ja muut tietorakenteet. Mutta Java on siin� mieless� erilainen tapaus, ett� Javassa kaikki luokat periytyv�t kantaluokasta Object, joten listaan voi ihan oikeasti laittaa kaikentyyppist� tietoja. Meid�n listamme toimii vain pelin sis�ll�, siihen ei voi esimerkiksi laittaa int-lukuja tai jonkun muun ohjelman olioita.
Huomaat varmaan my�s, ett� t�ll� toteutuksella voidaan kyll� samaan listaan laittaa lyijy� ja laastareita, mutta Sidepakkaus-oliota ei voida vahingossa k�sitell� kuin se olisi Ammus-olio. Siis tyyppiturvallisuus s�ilyy, vaikka lista voikin sis�lt�� eri tietotyyppien olioita. Sen lis�ksi, ett� t�m� ratkaisu ei ole yleist�tt�viss� kaikelle tiedolle, se on my�skin hitaahko. Varsinkin jos rakentaisimme monimutkaisempia rakenteita, ei polymorfismia k�ytt�en saavutettaisi huippusuorituskyky�.
Jos ongelmaan olisi olemassa unelmaratkaisu, se olisi t�m�nlainen: voitaisiin luoda koodia, joka pystyy k�sittelem��n kaikkea mahdollista tietoa, joka olisi tyyppiturvallinen ja joka tuottaisi yht� tehokasta k��nnetty� koodia kuin saman C++-koodin kirjoittaminen k�sin jokaiselle tietotyypille erikseen. Mutta ei vaan tule sellaista ratkaisua mieleen. Tietenkin yksi mahdollisuus olisi makrojen avulla rakentaa viritelm�, joka esik��nt�j�� k�ytt�en loisi useita versioita koodinp�tkist�, mutta makromagian harjoittaminen on aina ep�suotavaa, koska lopputulos on vaikeasti ymm�rrett�v� ja huonosti hallittava kyh�elm�, joka ei ole edes kunnolla tyyppiturvallinen. K�sink��n ei viitsisi aina luoda uutta versiota koodista, kun kerta t�ysin samaa logiikka vain sovelletaan uuteen tietotyyppiin.
Valitettavasti ei ole varaa my�sk��n palkata toista ohjelmoijaa tekem��n rutiininomaista koodin monistamista, eik� �lykk�ist� simpansseistakaan saa mill��n koulutettua hyvi� C++-ohjelmoijia (toiseen suuntaan kouluttaminen on joskus, tosin harvoin, mahdollista). Mutta IT-maailmassa unelmat k�yv�t toteen ja ongelmiin l�ytyy t�ydellinen ratkaisu; saanen esitell� mallit:
template <class T> class ListanSolmu { public: // saantifunktiot private: T* alkio; ListanSolmu* seuraava; };Mit� mallit oikeasti ovat?
Mallit ovat tekniikka, jonka avulla k��nt�j�n saa luomaan koodia k��nn�ksen yhteydess�. Kuuluu vain ilmavaa huminaa, kun ongelmien raskas pilviverho haihtuu pois ja p�iv� paistaa taas. Mallien avulla saadaan k��nt�j� luomaan automaattisesti listasta uusi versio kaikkia tarvittavia tietotyyppej� varten. Koska k��nt�j� on asialla, se voi hoitaa tyyppien tarkistuksen t�ysin normaalisti ja havaita kurjat ohjelmointivirheet jo k��nn�svaiheessa. Ja koska ty� tapahtuu k��nn�ksen aikana, on lopputuloksena syntyv� ohjelma yht� nopea kuin k�sin r��t�l�ity.
Tarkastellaan esimerkki� l�hemmin:
template <class T> class ListanSolmu { public: // saantifunktiot private: T* alkio; ListanSolmu* seuraava; };Template alussa tarkoittaa mallia. Emme siis t�ss� m��rittele luokkaa, vaan mallin jonka mukaan k��nt�j� osaa luoda uusia luokkia. Mik�li listaa ei k�ytet� ollenkaan, ei k��nt�j� luo yht��n
ListanSolmu<...
-luokkaa. Jos oliot ovat piparkakkuja ja luokat muotteja joilla ne tehd��n, ovat mallit valumuotteja jonka avulla piparkakkumuotit tehtaalla valmistetaan. Koodissa esiintyy eriskummallinen luokka T. Itseasiassa T ei ole luokka, vaan parametrisoitu tyyppi (T on m��ritelty class-tyyppiseksi eli tietotyypiksi, T voi siis olla luokka tai muuttujatyyppi). T on toisinsanoen tyyppi, joka voidaan sy�tt�� parametrina. Huomaa ett� T ei voi olla mik� tahansa tyyppi, vaan sen pit�� tukea kaikkia niit� operaatioita, joita malli sill� tekee. Jos T-oliosta kutsutaan metodia, jota sill� ei ole, antaa k��nt�j� luonnollisesti virheilmoituksen.Mallin avulla voidaan kertoa, ett� mink� tyypin listoja halutaan. Solmu-malliluokan instanssi ammuksia varten luotaisiin n�in:
ListanSolmu<Ammus> ammusSolmu;Lause itseasiassa luo kaksi asiaa: luokan ja olion.
ListanSolmu<Ammus>
saa k��nt�j�n luomaanListanSolmu<...>
-mallista instanssinListanSolmu<Ammus>
, siis uuden luokan. Sen j�lkeen luodaan olioammusSolmu
; t�h�n luokkaan. Jos olisimme tekem�ss� piparkakkuja, niin ensin kone muotoilisi luodin muotoisen piparkakkumuotin, jolla sitten painettaisiin yksi leivonnainen.Mit�p� tekisi seuraavanlainen koodi?
ListanSolmu<Ammus> ammusSolmu; ListanSolmu<Luotiliivi> luotiliivisolmuSolmu; ListanSolmu<Sidepakkaus> sideSolmu; ListanSolmu<Avainkortti> avainSolmu; ListanSolmu<Limsapullo> limsaSolmu; ListanSolmu<Vihollinen> vihollisSolmu; ListanSolmu<Rajahde> pommiSolmu; ListanSolmu<Tehtava> tehtavaSolmu; ListanSolmu<Pelaaja> tunariSolmu; ListanSolmu<Luodinreika> jalkiSolmu;No, aika monta luokkaa, nimitt�in kymmenen. Sit� koodia n�pyttelisi k�sin jo hetken jos toisenkin. Joidenkin mielest� mallien suuri ongelma on, ett� ne aiheuttavat koodin paisumista (code bloat). Siit�h�n t�ss�kin on kyse, kymmenen kertaa iso luokka on aika paljon koodia.
C++-k��nt�j� voi ja sen pit�isi osata optimoida t�m�nkaltaisia tilanteita: mik�li luokissa on identtisi� metodeja, niit� ei tarvitse monistaa vaan yhdell� p�rj��. Toki sellaiset metodit joissa k�sitell��n parametrisoitua tyyppi� tulee monistaa, koska ne k�sittelev�t erilaista tietoa eiv�tk� ole identtisi�. Mutta esimerkiksi oikeassa linkitetyn listan toteutuksessa suurin osa metodeista ei ole riippuvaisia listaan sijoitettujen alkioiden tyypist� (listan koko, onko lista lopussa? jne...) ja niinp� niit� ei tarvitse useita. Joten hyv� k��nt�j� monistaa vain ne metodit jotka on ehdottomasti pakko ja natinat koodin paisumisesta ovat kohtuullisen per�tt�mi�. Valitettavasti kaikki k��nt�j�t eiv�t ole hyvi�, joten tietty� varovaisuutta tulee noudattaa ettei k��nnetyn ohjelman koko paisu kuin pullasorsan maha.
Kun pohditaan millaista koodia mallit tuottavat, niin ei voi kuin todeta: s�p�kk��. Koska jokaiselle tietotyypille generoidaan oma koodinsa, voi k��nt�j� optimoida sen viimeiseen asti (toisin kuin k�ytett�ess� virtuaalifunktioilla toteutettua polymorfismia) ja syntyv� konekielinen ohjelma on nopeampi mit� keskiverto ohjelmoija saisi aikaan konekielist� koodia viilaamalla.
Yksi inhottava k�yt�nn�n puoli malleissa on. Koska k�ytett�ess� malleja k��nt�j� generoi C++-koodia ja yleisesti ottaen k��nt�j� ei ole kovin luova nime�m��n luokkia, voi tuloksena synty� hyvin kummallisia virheilmoituksia. T�m� on erityisen totta kun k�ytet��n C++:n mallikirjastoa, jossa malleja on k�ytetty ja paljon. Yksi virheilmoitus voi olla sivun pituinen eli toisinsanoen melko k�sitt�m�t�n. Hyv�t k��nt�j�t osaavat tuottaa fiksuja virheilmoituksia my�s malleja k�ytett�ess�, mutta varmasti pitki� itkuvirsi� luritteleviin k��nt�jiinkin t�rm��.
Esittelen nyt laajemman esimerkin mallien k�yt�st�. Siin� on vanha tuttu
ListanSolmu<...>
, jota k�ytt��LinkitettyLista<...>
- eli homma on hoidettu tyylikk��sti. Listasta instantioidaanint
- jaAmmus
-versiot, ja niill� hieman temppuillaan jotta n�hd��n miten homma toimii. Kuten huomaat, mallien kanssa koodista tulee melko monimutkaista. Olen kommentoinut esimerkki� paljon, jotta se varmasti avautuu. Suosittelen viett�m��n hetken jos toisenkin tuon pikku p�tk�n parissa, jotta mallien k�ytt� varmasti aukenee.#include <iostream> using std::cout; using std::endl; // Ensimm�inen malli, linkitetyn listan solmu. T�m� ei n�y listan k�ytt�j�lle. template <class T> class ListanSolmu { public: ListanSolmu(); // Kuten huomaat, C++ sallii pient� lepsuilua muodostimen nime�misess� //ListanSolmu<T>(); // Tarkkaan ottaen oikean niminen muodostin (toimii my�s) void asetaAlkio(const T* alkio); const T* annaAlkio(); void asetaSeuraava(ListanSolmu<T>* seuraava); ListanSolmu<T>* annaSeuraava(); private: const T* alkio; // Alkio on const T*, koska se ei voi muuttua ListanSolmu<T>* seuraava; // Seuraava solmu linkkien ketjussa }; // Kun mallimetodi m��ritell��n, ei se kuulu luokkaan vaan malliin // (eli ListanSolmu::ListanSolmu() ei kelpaa) template <class T> ListanSolmu<T>::ListanSolmu() { alkio = 0; seuraava = 0; } template <class T> void ListanSolmu<T>::asetaAlkio(const T* alkio) { this->alkio = alkio; // this, jotta luokan alkio ja parametrialkio eiv�t sekoitu } template <class T> const T* ListanSolmu<T>::annaAlkio() { return alkio; } // Huomaa, ett� seuraava:n tyyppi ei ole ListanSolmu. K��nt�j� pystyy p��ttelem��n // tyyppej� ja t�ss� k�visi my�s pelkk� ListanSolmu, mutta ListanSolmu<T> on tarkka // ja yleens� kannattaa m��ritell� aina mahdollisimman tarkasti mit� haluaa, k��nt�j�n // j�rjenlentoon ei ole aina luottaminen... template <class T> void ListanSolmu<T>::asetaSeuraava(ListanSolmu<T>* seuraava) { this->seuraava = seuraava; } template <class T> ListanSolmu<T>* ListanSolmu<T>::annaSeuraava() { return seuraava; } template <class T> class LinkitettyLista { public: LinkitettyLista(); void LisaaListaan(const T& alkio); const T& AnnaEnsimmainen(); const T& AnnaSeuraava(); bool OnkoViimeinen(); private: ListanSolmu<T>* alku; // Ensimm�inen solmu, ei sis�ll� listan tietoa ListanSolmu<T>* nykyinen; // Listan l�pik�ynti� varten }; template <class T> LinkitettyLista<T>::LinkitettyLista() { alku = new ListanSolmu<T>; // Tyhj� "otsikkosolmu" nykyinen = alku; } // LisaaListaan lis�� listan loppuun template <class T> void LinkitettyLista<T>::LisaaListaan(const T& alkio) { // Luo uusi solmu tietoa varten ListanSolmu<T>* uusi = new ListanSolmu<T>(); uusi->asetaAlkio(&alkio); // L�hde liikkeelle nykyisest� ja kulje listan loppuun asti ListanSolmu<T>* solmu = nykyinen; while (solmu->annaSeuraava() != 0) solmu = solmu->annaSeuraava(); // Lis�� listan jatkoksi solmu->asetaSeuraava(uusi); // P�ivit� nykyinen nykyinen = uusi; } template <class T> const T& LinkitettyLista<T>::AnnaEnsimmainen() { nykyinen = alku; // Otsikkosolmusta ei voi viel� ottaa alkiota koska se on tyhj� return AnnaSeuraava(); } template <class T> const T& LinkitettyLista<T>::AnnaSeuraava() { // Ota askel eteenp�in linkkien ketjussa nykyinen = nykyinen->annaSeuraava(); // Jos olemme tulleet loppuun, palauta nolla (muuntuu tyypiksi T) if (nykyinen == 0) return 0; // Palauta itse alkio return *(nykyinen->annaAlkio()); } // bool on kaksiarvoinen tietotyyppi (tosi / ep�tosi) // int k�visi my�s paluuarvon tyypiksi template <class T> bool LinkitettyLista<T>::OnkoViimeinen() { // Olemmeko yli listan lopun? return nykyinen != 0; } // Listan kokeilua varten class Ammus { int x; int y; int z; }; int main() { // Luo numerolista LinkitettyLista<int> pisteet; // Lis�� sinne muutamia lukuja pisteet.LisaaListaan(1); pisteet.LisaaListaan(2); pisteet.LisaaListaan(3); pisteet.LisaaListaan(4); pisteet.LisaaListaan(5); // Katsotaan tulevatko oikeat luvut ulos... for (int i = pisteet.AnnaEnsimmainen(); pisteet.OnkoViimeinen(); i = pisteet.AnnaSeuraava()) cout << i << endl; // Instantioidaan listasta my�s Ammus-versio (eli luokka LinkitettyLista<Ammus>) LinkitettyLista<Ammus> paukut; paukut.LisaaListaan(*new Ammus()); return EXIT_SUCCESS; }Mallien spesialisointi
Malleja ei voida soveltaa pelk�st��n luokkiin ja niiden metodeihin, vaan my�s yksitt�isiin funktioihin. On siis mahdollista luoda funktio summaa, jossa summattavien tyyppi on parametrisoitu:
template <class T> summaa(T t1, T t2) { return t1 + t2; } int i = summaa<int>(1, 2); // esimerkki funktion k�yt�st�Miten toimisi
summaa<string>
? No huonosti, koska se ei summaisi merkkijonoja vaan yhdist�isi ne. Meid�n tulisi siis pysty� ohjelmoimaan malli t�lle erikoistapaukselle. Aivan kuten luokka voidaan periytt�� ja siit� luoda erikoistunut lapsiluokka, my�s malleja voi spesialisoida. Spesialisoitu versio summaa-funktiosta voi k�ytt�� yleisemp�� versiota hyv�kseen, vain muuttaen merkkijonot luvuiksi ja tuloksen takaisin merkkijonoksi. Huomaa t�rke� ero t�ydellisen (complete spesialisation) ja osittaisen (partial spesialisation) spesialisoinnin v�lill�: t�ydellinen spesialisointi on luokka, osittainen taas malli jonka parametrisoinnit ovat vain yleist� mallia rajatumpia. Eli summaa<string> on t�ydellinen spesialisointi, koska siin� tyyppi ei ole mill��n tavalla parametrisoitu, kun taas summaa<T*> on osittainen, koska siin� tyyppi voi olla mit� vain, sill� rajoituksella ett� se on osoitin. T�ydellisess� spesialisoinnissa ei parametrilistassa saa olla tyyppej�, koska niit� ei k�ytet� mallin rungossa.#include<cstdlib> #include<iostream> #include<sstream> #include<string> // Yleinen summaa-malli template <class T> T summaa(T t1, T t2) { return t1 + t2; } // Koska teemme t�ydellisen spesialisoinnin, m��rittelemme paluuarvon // ja parametrit string-tyyppisiksi. Koska emme k�yt� parametrisoitua // tyyppi� T, se ei my�sk��n saa olla mallin parametrilistassa. // Vain osittainen spesialisointi saa k�ytt�� parametrisoitua tyyppi�, // (esim. T* olisi osittainen spesialisointi) - mutta t�ydellisess� // spesialisoinnissa parametrisointia ei tietenk��n tarvittaisikaan! template <> string summaa<string>(string t1, string t2) { // T�m� metodi itseasiassa tekee vain tyyppimuunnoksia // ja delegoi itse ty�n yleiselle mallille stringstream ss; // jotta voimme muuttaa intin stringiksi // kutsutaan yleisen version int-instanssia hoitamaan itse homma ss << summaa<int>(std::atoi(t1.c_str()), std::atoi(t2.c_str())); return ss.str(); // tyyppimuunnos takaisin } int main() { string kolme("3"), nelja("4"); std::cout << summaa<string>(kolme, nelja); return EXIT_SUCCESS; }Kuten todettiin, spesialisoinnin voi tehd� esimerkiksi kaikille osoittimille. T�llainen tarve tulee esille, mik�li malli vaatii operaatioita, jotka on m��ritelty (j�rkev�sti) osoittimen osoittamassa tyypiss�. Hyv� esimerkki on summaa. summaa<int*> palauttaa kahden osoittimen yhteenlasketun osoitteen, mik� on t�ysin hy�dyt�n tulos. Parempi olisi, ett� se palauttaisi osoittimen int-lukuun, joka on annettujen osoittimien osoittamien lukujen summa. Mik�li osoittimet ovat tyyppi� void*, ei tietenk��n mit��n summaa voida laskea ja silloin palautetaan tyhj� osoitin (osoitin, jonka arvo on 0). T�m� saataisiin aikaan seuraavilla malleilla (pelk�t esittelyt):
template <class T> summaa(T t1, T t2); template <class T> summaa<T*>(T t1, T t2); template <class T> summaa<void*>(T t1, T t2);K��nt�j� poimisi aina kaikista spesialisoiduimman version, eli
void*
-osoittimillevoid*
-version, muille osoittimille osoitinversion ja yleisen version lopuille. Huomaa ett� osoitinversion parametrit eiv�t ole T*-tyyppisi�, vaan T-tyyppisi�. Jos luodaansummaa<int*>
, sopii tyyppimaski T* tyyppiin int* niin, ett� T on int. Eli silloin summaa saisi int-parametreja.
Mallit k�yt�nt�jen parametrisoijana
Ehdottomasti yleisin tapa k�ytt�� malleja on tietorakenneluokkien luominen. Koska rakenteiden tulee toimia kaikilla tietotyypeill� ja koska niiden tulee olla nopeita, sopivat mallit kuin nyrkki silm��n. Toinen hyv� k�ytt�kohde malleille on k�yt�nt�jen parametrisointi (policy parametrization). Siis mahdollisuus p��tt�� jonkin toiminnon toteutus tarpeen mukaan. Esimerkki voisi olla mallifunktio "suositeltavampi":
template <class T> suositeltavampi(T t1, T t2) { if (ti > t2) return t1; else return t2; }Se valitsisi kahdesta sy�tteest� aina paremmin soveltuvan. Mutta soveltuvuus ei ole pelk�st��n suureemmuutta, olisi siis hyv� jos vertailuehto olisi asetettavissa tilanteen mukaan. Eli toisinsanoen olisi hyv�, jos vertailuk�yt�nt� olisi parametrisoitavissa. Voimmekin tehd� luokan, jossa on m��ritelty staattinen vertailumetodi ja joka voidaan antaa suositeltavampi-mallille parametrina. T�t� luokkaa kutsutaan k�yt�nt�luokaksi (policy class):
#include<iostream> #include<string> using std::string; using std::cout; using std::endl; // K�yt�nt�, joka suosii pidempi� rakenteita. // Toimii vain tyypeill� jotka sis�lt�v�t metodin "int size()". template <class T> class Pidempi { public: static bool ParempiKuin(T t1, T t2) { return t1.size() > t2.size(); } }; template <class T, class K> T suositeltavampi(T t1, T t2) { if (K::ParempiKuin(t1, t2)) return t1; else return t2; } int main() { string nimi1("Aki"); string nimi2("Aki-Petteri"); // Huomaa t�rke� v�lily�nti > > -merkkien v�liss�, ettei sit� // tulkita >>-operaattoriksi! string lapsenNimi = suositeltavampi<string, Pidempi<string> >(nimi1, nimi2); cout << "Kastan sinut " << lapsenNimi << "..." << endl; return EXIT_SUCCESS; }Hyvi� k�ytt�kohteita parametrisoiduille k�yt�nn�ille ovat juuri vertailut (esimerkiksi tietorakenteen j�rjestely suuruusj�rjestykseen) tai pysyvien olioiden instantiointi (haetaan verkon yli, luetaan tiedostosta, luetaan tietokannasta jne...). Huomaa, ett� my�s virtuaalifunktioita k�ytt�en voidaan saavuttaa sama asia. Virtuaalifunktiolla toteutettuna my�s k�yt�nt�� voidaan vaihtaa kesken suorituksen - toisin kuin mallien avulla. Mutta toisaalta mallit ovat rutkasti nopeampia, mik� on erityisen t�rke�� jos k�sitelt�v�n tiedon m��r� on suuri, kuten tietorakenteita tai suuria pysyvien olioiden laumoja k�sitelless� asia yleens� on.
Mallien monimutkainen k�ytt�
Mallien erikoisemmat k�ytt�mahdollisuudet perustuvat kahteen asiaan: mallien avulla ei voi parametrisoida pelk�st��n tyyppej� (esimerkiksi
class T
), vaan my�s ihan normaaleja muuttujia (kutenint i
). Lis�ksi muuttujan tyyppi voi olla mallissa parametrisoitu tyyppi, kunhan parametrisoidun tyypin m��rittely on ennen muuttujan m��rittely�. Pieni esimerkki selvent�nee asiaa:#include<iostream> template <class T, int koko, T oletusarvo> class Taulukko { public: Taulukko() { for (int i = 0; i < koko; i++) taulu[i] = oletusarvo; } T taulu[koko]; }; int main() { Taulukko<int, 2, 3> luvut; std::cout << luvut.taulu[1] << std::endl; // tulostaa 3... return 0; }N�in voi luoda taulukon, joka on sovellettavissa kaikille tyypeille ja josta voidaan luoda oikeankokoinen versio eri tarkoituksiin. Koska luominen ja koon asettaminen tapahtuu k��nn�ksen yhteydess�, voi k��nt�j� optimoida taulukon k�ytt�� paremmin kuin ajonaikaisia rakenteita. T�m� tarkoittaa my�s sit�, ett� koko- ja oletusarvo-parametrien tulee olla vakioita. Jokaiselle k�ytetylle eri tyypin, koon ja oletusarvon yhdistelm�lle luodaan oma luokkansa k��nn�ksen aikana, joten normaali muuttuja ei kelpaa: sen arvoa kun ei voi tiet�� viel� k��nn�ksen aikana. Vaikka t�llainen mallimagia onkin mahdollista, kannattaa kuitenkin usein mietti� onko se perusteltua. Mallien k�yt�ss� on yleens� paras rajoittua geneerisiin tietotyyppeihin ja tarvittaessa k�yt�nt�jen parametrisointiin.
Mit� mallit abstraktisti ovat? *
Periytt�misell� toteutetun polymorfismin avulla voidaan luoda koodia, joka pystyy k�sittelem��n useita eri tyyppej�. Siin� kaikki perustuu kantaluokkaan, eli koodin vaikutusalue voidaan ajatella puuna, jonka juurena on kantaluokka ja oksina periytyv� luokkahierarkia. Malleilla toteutetun koodin vaikutusalue on kaikkien niiden tietotyyppien lista, jotka tukevat kaikkia koodissa k�ytettyj� operaatioita - mit��n j�rke� operaatioissa ei tarvitse olla, kunhan ne on m��ritelty. Malleihin verrattuna polymorfismi on t�sm�ase, sen vaikutusalue on pienempi ja paljon rajatumpi: vaikka kantaluokkaa k�sittelev�n koodin kirjoittaja ei voikaan aina arvata, millaisia lapsiluokkia luokalla tulee olemaan, niin huomattavasti vaikeampi on mallia kirjoittavan ohjelmoijan arvata mink�laiset luokat tulevatkaan tukemaan mallin vaatimia operaatioita - ja miten j�rjett�m�sti ne sen tulevat tekem��n.
Itseasiassa mallien k�ytt�kin voidaan ajatella polymorfismina. Puhutaankin C++:n kahdesta erilaisesta polymorfismista, k��nn�ksenaikaisesta - tai parametrisoidusta - polymorfismista (mallit) ja ajonaikaisesta polymorfismista (periytt�minen ja virtuaalifunktiot). N�ill� kahdella on sek� k�yt�nn�llisi� ett� teoreettisia eroja. K�yt�nn�ss� mallit ovat nopeampia, mutta hieman hankalampia ja ep�intuitiivisempia.
Teoreettisesti t�rkein ero on vaikutusalueella. Ajonaikaisen polymorfismin vaikutusalue on pienempi, mutta ennen kaikkea hallittavampi. Koska virtuaalifunktioiden m��rittelyt rajoittavat huomattavasti lapsiluokkien toteuttajia, on polymorfisen koodin kirjoittajalla paljon selke�mpi ja rajatumpi kuva koodin vaikutuksesta kohteena oleviin tietotyyppeihin.
Yleens�, kuten my�s t�ss� oppaassa, polymorfismilla viitataan juuri ajonaikaiseen polymorfismiin ja mallien avuilla luotua monimuotoisuutta kutsutaan geneeriseksi ohjelmoinniksi. Geneerinen eli yleinen kuvaa hyvin mallien k�ytt��: malli eli parametrisoitu ohjelmanp�tk� antaa yleisen valtakirjan kaikille, jotka sit� haluavat k�ytt��. Perinteinen polymorfismi vaatii hyvin rajoittavasti, ett� kohteena oleva olio on jotakin tietty�: lapsiluokan olio on kantaluokan olio, siis PaloAuto on Auto. Vaikka polymorfinen koodi voikin k�sitell� PaloAutoja, KuormaAutoja ja PakettiAutoja, ne kaikki ovat Autoja. Vastakohtana geneerinen lista voidaan instantioida Autoille tai int*-osoittimille, eik� Auto ole int* tai int* ole Auto. Niill� ei ole juuri mit��n muuta yhteist�, kuin ett� niiss� on m��ritelty sijoitusoperaattori - mik� riitt��kin linkitetyn listan toteutukselle. T�m� yleisyys voi olla vaarallistakin ja johtaa v��rin toimivaan koodin: esimerkiksi yhteenlasku on normaali toimitus numeroille, mutta kahdella osoittimella tehtyn� ynn�ys ei tuota mit��n j�rkev�� - mutta on mahdollista!
Toki malleja on mahdollista spesialisoida ja luoda osoittimille oma versionsa, joka tuottaa j�rkevi� tuloksia. T�ss� kuitenkin havaitaan, ett� liian pitk�lle vietyn� p��dymme kopioimaan tietotyyppien v�lisi� suhteita (esim. luokkahierarkiat) omassa spesialisaatiorakenteessamme, mik� ei ole hyv� merkki - kaksinkertainen rakenne on tuhatkertainen p��nvaiva. Voikin havaita, ett� parametrisoiduilta tyypeilt� ei saa vaatia kovin useita tai monimutkaisia operaatioita - niit� tulisi k�ytt�� hyvin rajatulla tavalla. Mik�li t�h�n ei pystyt�, on hyv� ratkaisu tehd� v�liin erillinen kerros geneerisi� tyyppej�, joiden avulla operaatiot voidaan suorittaa. Esimerkki t�st� l�ytyy C++:n standardikirjastosta (STL:st�), jossa algoritmit kuten j�rjestely ovat yhteydess� tietorakenteisiin erillisten iteraattorien avulla.
Malleja ja perimist� on mahdollista yhdist��. Malliluokka voi peri� tavallisen luokan tai malliluokan, tavallinen luokka ei tietenk��n voi peri� malliluokkaa. Mutta mik�li periminen laajennetaan t�ysimittaiseksi olio-ohjelmoinniksi (mukaanlukien virtuaalifunktioiden k�ytt�), ei malleja kannata sotkea en�� mukaan. Parametrisoiduista virtuaalifunktioista tehdyt "tuplapolymorfiset" rakennelmat ovat hyvin vaikeita hahmottaa. Yleisesti ottaen olio-ohjelmointia ja geneerist� ohjelmointia ei kannatta yhdist�� - oliorakennelmat voivat kyll� k�ytt�� geneerist� kirjastoa (kuten STL:��) tai geneerinen ohjelma voi k�ytt�� oliopohjaista j�rjestelm��, kunhan niiden v�linen raja on selke�.
Geneerinen ohjelmointi *
Geneeriseen ohjelmointiin voi pureutua vertaamalla sit� olio-ohjelmointiin. Toki geneeriset ohjelmatkin sis�lt�v�t olioita ja luokkia, mutta olio-ohjelmoinnilla tarkoitan t�ss� koko konseptia, periytymist�, virtuaalifunktioita, luokkahierarkioita ja yleens� kaiken mallintamista olioiden avulla. Olio-ohjelmoinnissa abstraktit k�sitteet mallinnetaan kantaluokkien avulla (esim. Ajoneuvo). Geneerisess� ohjelmoinnissa taas abstraktiot eiv�t ole niin konkreettisia, vaan vaatimuksia siit� mit� operaatioita parametrisoidun tyypin tulee tukea (esimerkiksi taulukkomuotoisen tietorakenteen tulee tukea indeksointia eli []-operaattoria - eli taulukkomuotoisen tietorakenteen abstraktioon kuuluu vaatimus indeksoinnin tukemisesti).
Mik�li vaatimusta ei t�ytet�, johtaa se virheeseen k��nn�ksess�. Kaikkia vaatimuksia ei kuitenkaan voida mallintaa C++:n rakenteiden avulla. Esimerkiksi jos j�rjestelyfunktiolle annetaan j�rjestelt�v�n alueen alku- ja loppupiste, se ei pysty havaitsemaan jos pisteet sijaitsevat eri tietorakenteissa. Oliomallissa luokka Indeksi sis�lt�isi tiedon siit� mihin tietorakenteeseen se kuuluu, joten tarkistaminen olisi paljon helpompaa. Niinp� geneerisest� ohjelmoinnista voidaankin havaita hyvin yleinen ilmi�: mit� laajemmin joku asia on sovellettavissa, sit� voimattomampi se on. Sellainen perustuslaki, jonka kaikki maailman maat voisivat allekirjoittaa, saisi sis�lt�� vain harmaas�vyisen kansilehden.
Laajamittaisessa geneerisess� ohjelmoinnissa tuleekin soveltaa k�yt�nt��, jolla abstraktioiden vaatimukset voidaan dokumentoida selke�sti. Mit� vaaditaan tietorakenteelta? Ent� taulukolta? Geneerisen ohjelmoinnin yhteydess� puhutaan konsepteista, eli m��ritelmist� jotka kuvaavat kuinka eri osat ovat yhteydess� toisiinsa ja kuinka j�rjestelm��n voidaan lis�t� uusia ominaisuuksia. Jotta suurta geneerist� j�rjestelm�� voi hallita ja kehitt��, tulee konseptit dokumentoida selke�sti.
Geneerisess� ohjelmoinnissa p��roolissa eiv�t ole luokat. Esimerkiksi algoritmit - siis k�yt�nn�ss� mallifunktiot - ovat keskeisi�. Luokkien v�lill� ei my�sk��n ole juurikaan riippuvuuksia, kuten olio-ohjelmoinnissa. Jotta yleisi� operaatioita voidaan soveltaa luokkiin, luokkien tulee olla yksinkertaisia - joten niiden yhdistely, periytt�minen ja muut vastaavat monimutkaisuudet eiv�t ole k�ytett�viss�. J�rjestelm�n �lykkyys ei ole luokkien sis�ll�, vaan siin� miten algoritmeja sovelletaan tietorakenteisiin.
Geneerisille j�rjestelmille on yleist� k�yt�nt�jen parametrisointi. Toisaalta luokat ovat kapseloituja yksikk�j�, mutta toisaalta niist� on erotettu tiettyj� k�yt�nt�j�, jotka on parametrisoitu. K�yt�nn�t ovatkin luokan kapseloimaan k�sitteeseen verrattuna eri ulottuvuudessa - siis sill� miten tietorakenteita j�rjestell��n ei ole merkityst� erilaisten tietorakenteiden muodostamassa ulottuvuudessa. K�yt�nn�t ovatkin hyvin samantyyppinen ratkaisu kuin aspektit olio-ohjelmoinnissa.
Yhteenvetona voisi todeta, ett� geneerinen ohjelmointi ei ole er�s olio-ohjelmoinnin erityistapaus. Geneerinen tapa k�sitell� abstraktioita on erilainen ja huonommin C++-kielen tukema kuin oliomallinnuksessa. Niinp� geneerist� ohjelmointia ei tulisikaan soveltaa suuriin sovelluksiin: oliomallinnuksen on todettu auttavan suurten sovelluksien mallintamisessa, mutta geneerinen ohjelmointi ei sis�ll� yht� voimakkaita ominaisuuksia. Yleisk�ytt�isten kirjastojen tai alij�rjestelmien toteuttamiseen geneerinen ohjelmointi soveltuu hyvin, josta oiva esimerkki ovat yleiset tietorakenteet ja algoritmit, jotka C++:n STL-standardikirjastossa on toteutettu geneerisen ohjelmoinnin avulla.