Tässä kirjoitussarjassa käsittelen C++:aan liittyvää tietoutta, joka mielestäni kuuluu ammattimaisen C++-ohjelmoijan yleissivistykseen. Näin pystyy hyödyntämään kielen ominaisuuksia ylläpidettävän, luettavan ja vähän virheitä sisältävän ohjelmiston tuottamiseen, sekä pystyy lukemaan ja ylläpitämään näihin ominaisuuksiin pyrkivää C++-koodia. Käsittelen kielen ominaisuuksia, standardikirjaston osia sekä idiomeja ja muita sääntöjä ja käsitteitä.

Osan tietoudesta luulen olevan laajastikin tunnettua C++-ohjelmoijien keskuudessa; osa voi olla vieraampaa, mutta kaikista näistä tekniikoista on niin paljon etua, että ne mielestäni kuuluvat C++-ohjelmoijan sivistykseen.

Käsittelen juttusarjassa asioita, joita ei ole muissa suosituissa, vakiintuneissa kielissä kuin C++:ssa tai jotka ovat C++:ssa erilaisia kuin muissa kielissä. Tietenkin on monia enemmän kieliriippumattomia hyviä ohjeita, joita tulisi noudattaa myös C++:aa kirjoittaessa. En aio kuitenkaan käsitellä näitä tässä julkaisusarjassa.

Tyypin häivytys (type erasure)

Tyypin häivytys (oma suomennos, engl. type erasure) on eräs tekniikka, jonka avulla voidaan yhden staattisen tyypin avulla käsitellä kaikkia arvoja, jotka tukevat tiettyjä operaatioita, riippumatta niiden oikeasta, konkreettisesta tyypistä. Sillä on yhtäläisyyksiä niin olio-ohjelmoinnin rajapintaluokkien kuin mallien (engl. template) kanssa. Vaikka myös olio-ohjelmointi mahdollistaa erityyppisten arvojen käsittelyn yhden staattisen tyypin avulla, sitä ei lasketa tyypin häivytykseksi.

Esimerkki

Esimerkiksi voisi olla tyyppihäivytetty printable-tyyppi, jonka vaatimuksena sen toteuttaville tyypeille T on, että on olemassa operaattori std::ostream& operator<<(std::ostream&, const T&)¹. Tällaista luokkaa voisi käyttää seuraavasti:

void debug_out(printable p) {
    std::cout << p << std::endl;
}

struct id { int value; };
std::ostream& operator<<(std::ostream& stream, id theId) {
    stream << "id(" << theId.value << ")";
    return stream;
}

int main() {
    auto my_id = id{3};
    debug_out(my_id);
    debug_out("myos std::string kay"s);
    debug_out('a');  // Sisäänrakennetut tyypitkin käyvät
}

Koko esimerkki on myös saatavilla. Se vaatii C++14-kääntäjän; käännöstä on testattu GCC:n 4.9.2-versiolla seuraavalla komennolla:

g++ -Wall -Wextra -std=c++14 -Wpedantic -O2 type-erasure.cpp -o type-erasure

C++:n standardikirjastossa on myös yksi tyyppihäivytetty tyyppi tai oikeastaan joukko tyyppihäivytettyjä tyyppejä: std::function-mallin instanssit. Se on kuitenkin vaikeampi esimerkki juuri siitä syystä, että siinä on yksi epäsuoruuden taso lisää.

¹ Tarkalleen ottaen operaattori pitäisi määritellä sen mukaan, millä sitä voi kutsua; const T&:n sijaan operaattorin parametrina voisi olla yhtä hyvin T, kuten esimerkissä onkin.

Miksi

Toisin kuin rajapintaluokat tyypin häivytys mahdollistaa C++:n arvosemantiikkojen käytön. Tyypin häivytys ei sinänsä vaadi, että halutut operaatiot olisi toteutettava jäsenfunktioina. Lisäksi tyypin häivytys on ankkatyypityksen (engl. duck typing) mukainen, eli toteuttavien luokkien ei tarvitse erikseen mainita toteuttavansa jokin rajapinta. Seurauksena tyyppihäivytetyn tyypin vaatiman rajapinnan voi itse toteuttaa kolmannen osapuolen tyypille, jota ei pysty muokkaamaan, ja jopa kielen sisäänrakennetuille tyypeille.

Kaikki edellämainitut ominaisuudet ovat yhteisiä tyypin häivytykselle ja malleille. Malleihin verrattuna tyypin häivytyksen etuna on, että tyyppihäivytettyjä parametreja tai jäsenmuuttujia käyttävien funktioiden toteutusta ei tarvitse määritellä otsaketiedostossa, mikä lyhentää käännösaikoja ja vähentää tarvetta käännösyksiköiden uudelleenkäännökselle. Käännettyyn ohjelmaankaan ei ilmaannu useita versioita samasta funktiosta riippuen sen argumenttien konkreettisesta tyypistä. Lisäksi samaan tietorakenteeseen voidaan tallettaa objekteja, joiden konkreettinen tyyppi on eri. Tämä kaikki on seurausta siitä, että yksi ja sama käännösaikana käännetty koodi voi ajon aikana käsitellä useita eri konkreettisia tyyppejä.

Haittapuolena tyyppihäivytetyn tyypin määrittelyyn täytyy kirjoittaa enemmän koodia kuin vastaavan rajapintaluokan määrittelyyn. Tyyppikohtaista tosin on oikeastaan vain yksinkertaiset läpikutsut kyseisen tyyppihäivytetyn tyypin tarjoamille operaatioille. Uuden tyyppihäivytetyn tyypin tekeminen ei siis ole vaikeaa, jos on valmis pohja, jonka päälle tyypin voi tehdä. Tyyppihäivytetyn tyypin ylläpito on hieman työläämpää kuin rajapintaluokan ylläpito, sillä jokaista uutta operaatiota varten joutuu toteuttamaan 2–3 funktiota, jotka tosin sisältävät kukin vain yhden funktiokutsun.

Joissain tilanteissa ankkatyypitys saattaa olla huonompi vaihtoehto kuin vaatimus eksplisiittisestä rajapinnan toteuttamisesta. Tyyppihäivytetyn tyypin avulla ei ole mahdollista tehdä kaikkea sitä, mikä malleilla on mahdollista. Malleihin verrattuna kääntäjän mahdollisuudet optimoida jäävät usein huonommiksi, ja usein tallennustila joudutaan varaamaan free storesta eli keosta.

Miten

Sean Parent piti jokunen vuosi sitten erinomaisen esityksen aiheesta ja näytti, miten tyyppihäivytetty tyyppi toteutetaan käsin. Toteutus ei vaadi mitään virtuaalifunktioita sekä luokka- ja rakentajamalleja kummempaa, joten tyypin häivytystä voi käyttää jo C++98:ssa. C++98:ssa tosin tyypin häivytys ei ole yhtä käyttökelpoinen kuin modernimmilla kieliversioilla, sillä C++98:ssa konkreettisen tyypin täytyy olla kopioitavissa; C++11:sta alkaen ei välttämättä vaadita kuin siirrettävyyttä.

Kirjastojakin on ainakin Adobe Poly ja Boost Type Erasure; ohjeita molempien käyttöön. Kirjastoihin kannattaa ehkä tutustua sitten, kun osaisi tyyppihäivytyksen toteuttaa käsin.

En ole juuri tutustunut noihin kirjastoihin, mutta näyttäisi siltä, että niitä käyttämällä voi saada automaattisesti esimerkiksi pienten puskurien optimoinnin niin, että niitä varten ei tarvitse tehdä kekoallokaatiota. Lisäksi niiden avulla ei niin helposti lankea kopiointiin liittyviin sudenkuoppiin.

Yhteenveto

Tiivistettynä tyypin häivytys yhdistää arvosemantiikat ja ankkatyypityksen, jotka ovat C++:ssa muuten saatavissa mallien avulla², sekä dynaamisen polymorfismin, joka on C++:ssa muuten saatavissa periytymisen ja virtuaalifunktioiden avulla.

² Arvosemantiikat ovat toki saatavilla myös ilman malleja käyttämällä tavallisia, konkreettisia tyyppejä, mutta tällöin joudutaan luopumaan polymorfismista.