Kirjautuminen

Haku

Tehtävät

Oppaat: C++-ohjelmointi: Osa 8 - Mallit

  1. Osa 1 - Johdanto
  2. Osa 2 - Vakiot, muuttujat ja perustietotyypit
  3. Osa 3 - Ehdot, silmukat ja poikkeukset
  4. Osa 4 - Rakenteet, taulukot ja merkkijonot
  5. Osa 5 - Funktiot
  6. Osa 6 - Esittelyt, määrittelyt ja elinajat
  7. Osa 7 - Viittaukset, osoittimet ja dynaaminen muisti
  8. Osa 8 - Mallit
  9. Osa 9 - Luokkien perusteet
  10. Osa 10 - Luokkien erikoiset jäsenet
  11. Osa 11 - Luokkien perintä
  12. Liite 1 - C++-kehitysympäristöt
  13. Liite 2 - Valmis C++-kääntäjäpaketti

Kirjoittaja: Metabolix. Vuosi: 2009.

Suuren koodimäärän toistaminen useaan kertaan ei ole juuri koskaan hyvä asia: ohjelmasta tulee pitkä ja mutkikas, ja jos toistettua koodia muutetaan, muutokset pitäisi muistaa tehdä kaikkiin kohtiin. Usein toiston voi välttää yksinkertaisesti sijoittamalla koodin funktioon ja kutsumalla samaa funktiota eri kohdista, mutta jos osassa tilanteista käytetäänkin eri tietotyyppejä, sama funktio ei enää sovi. Funktioista ja tietueista voi kuitenkin kirjoittaa malleja (engl. template), joiden avulla kääntäjä pystyy luomaan samasta koodista versioita esimerkiksi eri muuttujatyypeille. Mallit ovat yksi C++:n monimutkaisimmista asioista, ja siksi tässä vaiheessa käsitellään vain niiden perusidea ja joitain yksinkertaisia käyttötapoja.

Yleiskäyttöiset funktiot

Kaikille tavallisille lukutyypeille, siis erilaisille kokonaisluvuille ja liukuluvuille, on loogista määritellä joitakin tavallisia matemaattisia funktioita kuten itseisarvo. Normaalin kokonaisluvun itseisarvofunktio näyttäisi tältä:

int itseisarvo(int x) {
	// Negatiivisen luvun itseisarvo on sen vastaluku.
	if (x < 0) {
		return -x;
	}
	// Muuten itseisarvo on luku itse.
	return x;
}

Olisi turhaa työtä kirjoittaa edes näin lyhyt funktio erikseen kaikille eri tyypeille (short, int, long, float, double, long double). Kun siitä sen sijaan kirjoitetaan malli, kääntäjä osaa automaattisesti luoda vastaavan funktion mille tahansa tyypille. Itseisarvofunktion malli kuuluisi sanallisessa muodossa näin: "Ota jokin x; jos se on nollaa pienempi, palauta -x, muuten palauta x." Kun kääntäjälle kerrotaan, että jokin tarkoittaa tyyppiä int, kääntäjä osaa tehdä äskeisen funktion, jossa käsitellään parametria int x. Samalla tavalla onnistuvat myös float-, double- ja muut mainitut versiot funktiosta.

Malli alkaa sanasta template, jonka perään kulmasulkeisiin luetellaan malliparametrit samaan tapaan kuin funktion parametrit eli pilkuilla eroteltuina. Parametrit ovat kuitenkin hyvin erityyppisiä kuin funktioilla: yleisimmin mallin parametrin tyyppi on tietotyyppi (typename tai class; nämä ovat aivan sama asia). Kun mallia käytetään, sille annetaan argumenttina jokin oikea tietotyyppi, esimerkiksi int tai float. Seuraavassa esimerkissä on itseisarvofunktion malli, josta luodaan kaksi eri versiota funktiosta.

#include <iostream>

// Mallin parametrina on jokin tietotyyppi. Parametrin nimi on T.
template <typename T>
// Itse funktio kirjoitetaan nyt niin, että tietotyyppinä ei ole int vaan T.
T itseisarvo(T x) {
	// Negatiivisen luvun itseisarvo on sen vastaluku.
	if (x < 0) {
		return -x;
	}
	// Muuten itseisarvo on luku itse.
	return x;
}

int main() {
	int luku;
	std::cout << "Anna kokonaisluku: ";
	std::cin >> luku;
	// Käytetään mallia. Annetaan sille argumenttina int; mallista
	// muodostuu funktio, jossa T on korvattu todellisella tyypillä int.
	int tulos = itseisarvo<int>(luku);
	std::cout << "Luvun itseisarvo on " << tulos << std::endl;

	float liukuluku;
	std::cout << "Anna liukuluku: ";
	std::cin >> liukuluku;
	// Tällä kertaa annetaan kääntäjän valita oikea versio funktiosta.
	// Koska funktiolle annettava argumentti on tyypiltään float,
	// kääntäjä käyttää automaattisesti funktiota itseisarvo<float>
	// eli korvaa mallista T:n tyypillä float.
	float liukutulos = itseisarvo(liukuluku);
	std::cout << "Luvun itseisarvo on " << liukutulos << std::endl;
}

Kun mallia käytetään, kääntäjä generoi siitä todellisen funktion valituilla tyypeillä. Esimerkin itseisarvofunktiosta tulee lopulliseen ohjelmaan kaksi versiota, joista ensimmäinen vastaa oppaan alussa ollutta int-tyypin itseisarvofunktiota ja toinen samaa funktiota float-tyypille. Itse mallia ei ole lopullisessa ohjelmassa jäljellä eikä uusia versioita funktiosta luoda kesken ohjelman, vaan kaikki tapahtuu etukäteen. (Standardin mukaan tämä ei pidä aivan paikkaansa. Asiaan pitäisi voida vaikuttaa export-sanalla mallin edessä, mutta yleisimmät kääntäjät eivät välitä siitä, koska ominaisuus olisi vaikea toteuttaa järkevästi.)

Tyypin määräytyminen

Äskeinen funktio otti vain yhden luvun, joten kääntäjän oli helppo päätellä oikea tyyppi T. Seuraava funktio on mutkikkaampi: se ottaa kaksi lukua ja palauttaa niistä suuremman. Nyt syntyykin ongelma: mitä tapahtuu, jos funktiolle annetaan kaksi erilaista lukua?

Jos ei ole aivan selvää, mitä ohjelmoija haluaa tehdä, aiheutuu käännösvirhe. Tässä esimerkissä funktiolla on kaksi parametria, joiden tyyppinä on sama T. Niinpä funktion todellisten argumenttienkin pitää olla samaa tyyppiä, ellei koodissa erikseen määrätä, mitä tyyppiä T tarkoittaa.

#include <iostream>

// Funktio ottaa kaksi samanlaista lukua ja palauttaa pienemmän.
template <typename T>
T pienempi(T x, T y) {
	if (x < y) {
		return x;
	}
	return y;
}

int main() {
	int a, b;
	std::cout << "Anna luku A: ";
	std::cin >> a;
	std::cout << "Anna luku B: ";
	std::cin >> b;

	// Otetaan liukuluku A:n ja B:n väliltä.
	float c = (a + b) / 2.0 + 0.2;
	std::cout << "C:n arvo on  " << c << std::endl;

	// Parametrit ovat int-tyyppisiä, joten kaikki on selvää.
	std::cout << "Pienempi luvuista A ja B on " << pienempi(a, b) << std::endl;
	// Jos float erityisesti muutetaan int-tyyppiseksi, tilanne on yhä sama.
	std::cout << "Pienempi luvuista A ja (int) C on " << pienempi(a, (int) c) << std::endl;

	// Seuraavaksi tulee virhe: kääntäjä ei tiedä, olisiko T int vai float!
	// Kokeile, millaisen viestin kääntäjäsi tästä virheestä antaa.
	//std::cout << "Pienempi luvuista A ja C on " << pienempi(a, c) << std::endl;

	// Kerrotaan kääntäjälle, että halutaan float-versio.
	std::cout << "Pienempi liukuluvuista A ja C on " << pienempi<float>(a, c) << std::endl;
	std::cout << "Pienempi liukuluvuista B ja C on " << pienempi<float>(b, c) << std::endl;

	// Kerrotaan kääntäjälle, että halutaan int-versio.
	std::cout << "Pienempi kokonaisluvuista A ja C on " << pienempi<int>(a, c) << std::endl;
	std::cout << "Pienempi kokonaisluvuista B ja C on " << pienempi<int>(b, c) << std::endl;
}

Tietuemallit

Malleja voi käyttää myös tietueisiin ja luokkiin. Aiemman oppaan piste-tietueesta voi tehdä vaikkapa mallin, jonka parametrina on koordinaattien tietotyyppi. Näin saadaan samalla vaivalla liukulukukoordinaatistoon ja kokonaislukukoordinaatistoon sopivat tietueet.

template <typename koordinaatti>
struct piste {
	koordinaatti x, y;
};

/*
 * Nyt esimerkiksi piste<int> vastaa tyyppiä, jolla on int-jäsenet x ja y.
struct piste_int {
	int x, y;
};
*/

// Määritellään kaksi muuttujaa. Tietuemalleille täytyy itse antaa oikea
// tyyppi argumenttina, koska kääntäjä ei voi päätellä sitä mistään.
// Pisteen a koordinaatit ovat kokonaislukuja:
piste<int> a,

// Pisteen b koordinaatit ovat liukulukuja:
piste<float> b;

Esimerkin muuttujat ovat erityyppisiä, koska malleille on annettu eri parametrit.

Oletusargumentit

Malliparametreille, kuten funktioidenkin parametreille, voi määrätä oletusarvoja eli oletusargumentteja. Äskeiselle piste-mallille voisi määrätä oletustyypiksi float, jolloin piste<> toimisi samalla tavalla kuin piste<float>. Jos mallilla on useita parametreja, oletusarvoksi sopii aiemman parametrin arvo. Tätä ominaisuutta käytetään seuraavassa esimerkissä.

#include <iostream>

// Tällä mallilla on kaksi parametria, ja tietueeseen tulee kaksi jäsentä.
// T:n oletusarvo on float, ja U:n oletusarvo on T.
template <typename T = float, typename U = T>
struct pari {
	T a;
	U b;
};

// Nimetään typedef-riveillä muutama uusi tyyppi.
// pari<> olkoon piste_ab. Koska T = float ja U = T, jäsenet ovat float-tyyppisiä.
typedef pari<> piste_ab;

// pari<piste_ab> olkoon jana. T = piste_ab ja U = T.
typedef pari<piste_ab> jana;

// Tämä mallifunktio tulostaa saamansa arvon. Jos mallia yritetään käyttää
// sellaisella tyypillä T, ettei sitä voi tulostaa totuttuun tapaan, ohjelma
// ei käänny. Tämän funktion ei ole tarpeen muuttaa parametriaan, joten
// käytetään viittausta ja const-sanaa.
template <typename T>
void tulosta(const T& t) {
	std::cout << t;
}

// Tehdään myös kuormitettu malli, joka osaa tulostaa pareja.
// Nyt malliparametrit välitetään edelleen pari-mallille.
// Kun tulosta-funktiota kutsutaan pari-tyyppisellä parametrilla,
// kääntäjä käyttää tätä mallia ja päättelee T:n ja U:n arvot
// tulostettavan parin perusteella.
template <typename T, typename U>
void tulosta(const pari<T, U>& p) {
	// Tulostetaan rekursiivisesti a ja b muodossa (a, b)
	// Jos a tai b on pari, kutsutaan tätä samaa funktiota uudestaan.
	// Muuten kutsutaan tulosta-funktiota, joka tulostaa suoraan arvon.
	std::cout << "(";
	tulosta(p.a);
	std::cout << ", ";
	tulosta(p.b);
	std::cout << ")";
}

int main() {
	// Janassa on kaksi päätepistettä, joilla on kaksi koordinaattia.
	jana j = {{1.3, 2.7}, {3.015, 4.945}};
	j.a.a = 6.21;

	// Tulostetaan jana. Kääntäjä valitsee jälkimmäisen mallin; se
	// sopii paremmin parametrin tyyppiin (pari-mallista luotu jana).
	// Tämä funktio tulostaa pista_ab-tyyppiset jäsenet, jotka myös
	// tulostetaan pari-malliinerikoistuneella versiolla.
	// Pisteen jäsenet ovat kuitenkin float-tyyppisiä, joten niihin
	// käytetäänkin ensimmäistä mallia.
	std::cout << "Jana: ";
	tulosta(j);
	// Janan tulostaminen tapahtuu seuraavan kaavion mukaan:
	// tulosta(j):
	//     tulosta(j.a):
	//         tulosta(j.a.a): 6.21
	//         tulosta(j.a.b): 2.7
	//     tulosta(j.b):
	//         tulosta(j.b.a): 3.015
	//         tulosta(j.b.b): 4.945
	// Loppuun on hyvä tulostaa rivinvaihto; tulosta-funktio ei sitä tee.
	std::cout << std::endl;

	// Mallin parametreille voi antaa myös eri arvot.
	// Luodaan pari, jossa on kokonaisluku ja tekstiä. Laitetaan luvuksi
	// vaadittava pistemäärä ja tekstiksi arvomerkissä komeileva eläin.
	pari<int, std::string> merkit[] = {
		{10, "papukaija"},
		{22, "leppäkerttu"},
		{8, "hämähäkki"},
	};
	const int maara = sizeof(merkit) / sizeof(merkit[0]);
	for (int i = 0; i < maara; ++i) {
		std::cout << merkit[i].a << " pistettä ja " << merkit[i].b << "merkki!" << std::endl;
	}
}

Loppusanat

Tässä oppaassa käsiteltiin vasta helpot perusasiat malleista. Kaikki esimerkit eivät olleet käytännöllisiä, koska todella käytännölliset sovellukset malleille ovat usein melko pitkiä. Joitain esimerkkejä näistä on Ohjelmointiputkan koodivinkeissä. Mallit pääsevät kunnolla oikeuksiinsa vasta luokkien kanssa – niistä kerrotaankin jo seuraavassa oppaassa. Myös C++:n standardikirjasto on pullollaan hyödyllisiä funktio- ja luokkamalleja, joista on tekeillä erillinen opassarja.


Kommentit

fronty [29.08.2009 14:15:55]

Lainaa #

Ensimmäinen ajatus ku näin ton osan nimen oli aika vahva wtf. :D Ihan outoja nää suomenkieliset termit.

nerootto [30.08.2009 12:47:27]

Lainaa #

Sekalainen toi vika koodi.

Jaska [01.09.2009 15:25:31]

Lainaa #

Template on suomeksi malli, kts. Stroustrup: C++-ohjelmointi.

Metabolix [01.09.2009 16:05:10]

Lainaa #

Kiitokset Jaskalle paremmasta suomennoksesta. Itse olen törmännyt yleensä templaatteihin ja toisinaan kaavaimiin, joten valitsin tämän suomennoksen, mutta koska Google tuki uutta käännöstä, korjasin opasta sen mukaan.

Jaska [02.09.2009 21:38:43]

Lainaa #

Sana kaavain jäi vielä sivulle http://www.ohjelmointiputka.net/index.php

Kirjoita kommentti

Huomio! Kommentoi tässä ainoastaan tämän oppaan hyviä ja huonoja puolia. Älä kirjoita muita kysymyksiä tähän. Jos koodisi ei toimi tai tarvitset muuten vain apua ohjelmoinnissa, lähetä viesti keskusteluun.

Muista lukea keskustelun ohjeet.
Tietoa sivustosta