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 luomaan ListanSolmu<...>-mallista instanssin ListanSolmu<Ammus>, siis uuden luokan. Sen j�lkeen luodaan olio ammusSolmu; 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 instantioidaan int- ja Ammus-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*-osoittimille void*-version, muille osoittimille osoitinversion ja yleisen version lopuille. Huomaa ett� osoitinversion parametrit eiv�t ole T*-tyyppisi�, vaan T-tyyppisi�. Jos luodaan summaa<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 (kuten int 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.

 

Takaisin