Kirjoittaja: Metabolix
Kirjoitettu: 08.02.2009 – 14.10.2015
Tagit: koodi näytille, vinkki
Aiemmin on kovasti keskusteltu siitä, kuinka kaksiulotteinen taulukko luodaan helpoiten dynaamisesti. On kuitenkin asioita, jotka tuottavat ongelmia staattisillakin taulukoilla. Yksi näistä on taulukon välittäminen funktiolle: moniulotteista taulukkoa varten funktion pitää tietää sen kaikki mitat ensimmäistä lukuun ottamatta. Jos siis ohjelma käyttää muutamaa eri taulukkokokoa, jotka kuitenkin ennalta tiedetään, täytyisi jotkin funktiot kirjoittaa uudestaan kullekin näistä.
Ongelmaan on muutamakin ratkaisutapa. Funktiot voi toteuttaa kaavaimilla, kuten koo näyttää vinkissä Taulukon koko funktiokutsussa. Toinen tapa on tehdä taulukoista yksiulotteisia, jolloin oikean "kaksiulotteisen" kohdan laskeminen onnistuu yksinkertaisella kaavalla. Tämänkin koodin voi piilottaa apuluokkaan. Kolmas vaihtoehto on varata todella taulukollinen taulukoita, kuten tässä C-kielisessä vinkissä tehdään.
Neljäs mahdollisuus esitellään tässä vinkissä, ja se poikkeaa hieman muista mainituista. Siinä nimittäin taulukko säilytetään luokan sisällä, jolloin sitä voi käsitellä jäsenfunktioilla. Taulukko on kooltaan staattinen, ja koko määritellään kaavainten avulla. Abstraktin rajapinnan ja virtuaalisten funktioiden voimin sitä voi kuitenkin käsitellä myös tietämättä kokoa etukäteen. Koodissa rajapintana toimiva luokka on taulu2d ja varsinainen taulukkoluokka on taulu2d_impl.
#include <stdexcept> /** * Abstrakti kantaluokka. * Tässä määritellään kaikki funktiot puhtaasti virtuaalisiksi, jolloin * luokasta ei voi luoda yhtään ilmentymää. Luokkaan voi kuitenkin luoda * viittauksia, jotka todellisuudessa viittaavat muun kokoiseen taulukkoon. */ template <typename T> class taulu2d { public: virtual int leveys() const noexcept = 0; virtual int korkeus() const noexcept = 0; virtual T& operator () (int x, int y) = 0; virtual T const& operator () (int x, int y) const = 0; }; /** * Kaavain staattisen kokoisille kaksiulotteisille taulukoille: * T: tyyppi; L, K: taulukon mitat. * Luokka periytetään 0x0-taulukosta, jotta voidaan laillisesti antaa funktioille * viittauksia erikokoisiin taulukoihin käyttämällä abstraktia pääluokkaa. */ template <typename T, int L, int K> class taulu2d_impl: public taulu2d<T> { // Itse data. T taulu[K][L]; public: // Funktiot mittojen hakemiseen. virtual int leveys() const noexcept { return L; } virtual int korkeus() const noexcept { return K; } // Funktio (operaattori) datan hakemiseen kohdasta (x, y). // Viittauksen palauttaminen mahdollistaa myös sijoituksen: // taulu(x, y) = muuttuja; virtual T& operator () (int x, int y) { if (0 <= x && x < L && 0 <= y && y < K) { return taulu[y][x]; } // Virhetilanteessa heitetään poikkeus. throw std::out_of_range("Virheellinen taulukon kohta!"); } // Funktio datan hakemiseen; const-versio. Tätä tarvitaan, // jotta const-viittaukset luokkaan toimisivat. virtual T const& operator () (int x, int y) const { if (0 <= x && x < L && 0 <= y && y < K) { return taulu[y][x]; } throw std::out_of_range("Virheellinen taulukon kohta!"); } };
Esimerkkejä
/** * Funktio, joka osaa täyttää tällaisen taulukon arvolla. * Parametriksi annetaan viittaus yhteiseen kantaluokkaan. * Virtuaalisten funktioiden ansiosta funktiokutsut menevät * oikealle (tietyn kokoiselle) taulukkoluokalle. */ template <typename T> void tayta(taulu2d<T>& taulu, T arvo) { for (int y = 0; y < taulu.korkeus(); ++y) { for (int x = 0; x < taulu.leveys(); ++x) { taulu(x, y) = arvo; } } } /** * Funktio, joka laskee int-taulukosta lukujen summan. * Taulua ei muokata, joten otetaan const-viittaus. */ int summa(taulu2d<int> const& taulu) { int summa = 0; for (int y = 0; y < taulu.korkeus(); ++y) { for (int x = 0; x < taulu.leveys(); ++x) { summa += taulu(x, y); } } return summa; } #include <iostream> int main() { // Tehdään 2x3-taulukko. taulu2d_impl<int, 2, 3> t; // Täytetään taulukko kolmosilla. tayta(t, 3); // Tulostetaan lukujen summa (2 * 3 * 3 = 18). std::cout << "Taulukon lukujen summa: " << summa(t) << std::endl; // Tarkistetaan, että väärään kohti tökkiminen aiheuttaa virheen. try { t(-1, 8) = 3; } catch (std::out_of_range& err) { std::cerr << "Poikkeus! " << err.what() << std::endl; } return 0; }
Tämä vinkki valottaa mukavasti, mitä templateilla ja luokkien periyttämisellä voi tehdä.
On sitten vain vähän eri asia se, mitä oikeasti kannattaa tehdä, ja siinä mennään vähän hakoteille. Mutta koodi on toki luettavaa ja siitä pystyy tajuamaan, millaista meininkiä on meinattu.
Ratkaisuna staattinen tilavaraus on kyllä näppärä ja nopsa, mutta toisaalta kaikki taulukon käsittely tapahtuu sitten periaatteessa virtuaalifunktioiden avulla. Taulukoiden kantaluokkana on 0x0-kokoinen taulukko, joka on kyllä aika outo otus. Koska käytetään periyttämistä, voidaan vielä lisäksi sanoa, esimerkiksi että "5x7-kokoinen taulukko on 0x0-kokoinen taulukko", sillä sitähän public-perintä tarkoittaa.
Pitäisi ehkä ajatella enemmän, mihin ja miten (kaksiulotteisia) taulukoita on tarkoitus käyttää, eli lähteä enemmän käyttötarpeista. Mutta vinkkitarpeisiin esimerkki käy sopivasti.
Tapauksen (L,K)=(0,0) spesialisointi on hauska kikka, mutta minusta olisi selkeämpää laittaa rajapinta ja toteutus erinimisiin luokkiin. On epäintuitiivista, että 0x5- ja 5x0-kokoiset taulukot käyttäytyvät eri tavalla kuin 0x0-kokoiset.
template <typename T> class abstaulu2d { ... }; template <typename T, int L, int K> class taulu2d: public abstaulu2d<T> { .... };
Tässä versiossa ei ole tarvetta antaa L:lle ja K:lle oletusarvoja, joten taulu2d<int,5>
johtaa selkeään virheilmoitukseen.
Poikkeusmääritteitä ei muuten (enää) kannata käyttää; C++11-aikakaudella ne on deprekoitu ja niiden poistamista kielestä harkitaan.
koo ja jlaire, aiheellisia huomioita, päivitin vinkkiä.