Tyyliseikkoja

C++:lla on mahdollista kirjoittaa hyvinkin kryptistä koodia, mutta turhaa kryptisyyttä on syytä välttää. Ennemminkin tulee pyrkiä kirjoittamaan selkeää koodia, jota on helppo lukea ja ymmärtää. Tätä kautta myös ohjelmien ylläpito helpottuu.

Edellä tuli mainittua joitakin ohjelmien laatuvaatimuksia (selkeys, luettavuus, ymmärrettävyys, ylläpidettävyys). Muita tärkeitä asioita ovat ohjelman toimivuus ja tehokkuus. Laatuvaatimuksille ominaista on, että ei ole yhtä ainoaa oikeaa tapaa kirjoittaa tyylikästä koodia.

Alla olevissa kohdissa luetellaan tällä kurssilla käytössä olevia tyylisääntöjä, joihin projektien arvostelussa kiinnitetään huomiota. Tavoitteena toki on esittää mahdollisimman yleispäteviä sääntöjä, joista olisi hyötyä laajemminkin kuin vain tällä kurssilla.

Toimivuus

Ohjelman toimivuuden voi ajatella olevan kaikkein olennaisin laatuvaatimus. Mutta jos ohjelma toimii oikein, onko sitten mitään merkitystä, miltä ohjelma näyttää tai miten ymmärrettävää koodi on? Kuitenkin jos ohjelman rakenne on looginen ja mahdollisimman yksinkertainen, se on myös helpompi saada toimimaan oikein.

Alla olevasta listasta löytyy joitakin toimivuuteen liittyviä vaatimuksia:

  • Ohjelman on toimittava oikein kaikilla sallituilla syötteillä.
  • Ohjelman on ilmoitettava virheellisistä syötearvoista.
  • Jos ohjelmaa yritetään käyttää jotenkin väärin, ohjelman on varoitettava käyttäjää.
  • Tarkistusten on oltava riittävän kattavia, niin että ohjelma ei kaadu millään syötearvoilla vaan päättyy hallitusti.
  • Ohjelman ei pidä kysyä sellaisia tietoja, jotka se voi itse laskea.
  • Ohjelman käytön pitää olla mahdollisimman helppoa ja havainnollista.

Strukturointi (funktiot)

Strukturoinnilla tarkoitetaan tapaa jakaa ohjelma osiin. Tarkoituksena on, että nämä osat ovat riittävän pieniä ja yksinkertaisia sekä helposti hallittavia.

Strukturointi koskee mitä tahansa ohjelman osaa, esimerkiksi funktioita ja luokkia.

Erityisesti funktiot eivät saa olla liian pitkiä eivätkä myöskään liian leveitä (rivit eivät saa olla liian pitkiä). Funktion pitäisi mahtua näytölle kokonaan.

Toisaalta funktio ei saisi myöskään olla liian lyhyt. Yleensä 1-rivinen funktio tai funktio, joka vain kutsuu toista funktiota, on liian lyhyt. Poikkeuksena tästä ovat luokkien getter-metodit, jotka palauttavat luokan attribuutin arvon. 1-rivinen funktio on hyväksyttävä myös, jos se parantaa luettavuutta, esimerkiksi alla oleva funktio antaa monimutkaiselle ehdolle nimen:

bool is_snake_dead()
{
    return new_head.x < 0 or new_head.x >= width_ or
           new_head.y < 0 or new_head.y >= height_);
}

Toisteista koodia pitää välttää. Tällä tarkoitetaan saman tai melkein saman koodin kirjoittamista useaan kertaan. Toisteista koodia voi välttää esimerkiksi funktioiden avulla tai parametrisoimalla funktioita.

Nimeäminen

C++ on merkkikokoriippuvainen (case-sensitive), eli isot ja pienet kirjaimet tulkitaan eri merkeiksi. Vaikka siis olisikin mahdollista käyttää tunnisteita nimi ja Nimi samassa näkyvyysalueessa, niin ohjelmointityylin kannalta tämä ei ole hyvä ratkaisu. Muutenkin liian samanlaisia nimiä kannattaa välttää, koska ne voi helposti sekoittaa toisiinsa.

Muuttujien, vakioiden, funktioiden, tyyppien, luokkien ym. tunnisteiden nimien tulee olla kuvaavia. Ei haittaa, vaikka nimet olisivat pitkiä.

Tyypillisesti muuttujien ja funktioiden nimet alkavat pienellä kirjaimella, luokkien ja tietueiden (struct) nimet taas isolla kirjaimella. Vakiot kirjoitetaan kokonaan isoilla kirjaimilla ja erottimena käytetään alaviivaa.

Funktioiden nimeämisessä käytetään pääsääntöisesti kahta tapaa: printField ja print_field. Näistä ensiksi mainittu on suositeltavampi, mutta joka tapauksessa samassa ohjelmassa tulee käyttää yhtenäistä tyyliä. Funktioiden niminä tulee käyttää komentoja kuten print (tai tulosta) eikä substantiiveja kuten printing (tai tulostaminen).

Totuusarvoisten (bool) funktioiden nimien suositellaan alkavan merkkijonolla is (tai onko). Tällöin jo nimestä voi päätellä, missä tilanteessa funktio palauttaa arvon true ja missä arvon false. Tästä syystä esimerkiksi isReady on parempi nimi totuusarvoiselle funktiolle kuin checkStatus. Funktio voi kuitenkin palauttaa totuusarvon tietona siitä, onnistuiko kyseinen operaatio vai ei. Tällöin edellä kuvattua totuusarvoisten funktioiden nimeämissääntöä ei tarvitse noudattaa. Esimerkiksi funktion setValue pääasiallisena tarkoituksena on asettaa arvo, mutta se voi silti palauttaa totuusarvon: arvon true operaation onnistuessa ja arvon false muulloin.

Luokkien niminä tulee käyttää pääsääntöisesti yksikkömuotoa. Juuri koskaan ei ole mitään syytä käyttää monikkoa, sillä luokka yleensä kuvaa yhden olion ominaisuudet. Luokasta voi sitten olla (ja yleensä onkin) useita ilmentymiä.

Jäsenmuuttujien nimissä on hyvä käyttää viimeisenä merkkinä alaviivaa. Näin ne on helpompi erottaa muista muuttujista.

Nimettyjä vakioita pitäisi käyttää taikanumeroiden sijaan. Koodin lukeminen on helpompaa, jos siinä esiintyy vaikkapa vakio NUMBER_OF_STUDENTS numeron 600 sijasta. Nimettyjen vakioiden etuna on myös se, että jos vakion arvoa halutaan muuttaa, muutoksen joutuu tekemään vain yhteen kohtaan koodissa. Myös vakioilla pitää olla käyttötarkoitusta kuvaavat nimet, ei siis ole järkevää määritellä: const int HUNDRED = 100.

Kommentointi

Kommenteilla voidaan parantaa koodin ymmärrettävyyttä. Toisaalta ymmärrettävyyttä voidaan parantaa myös sillä, että muuttujille annetaan kuvaavat nimet. Tästä löytyy esimerkki Steve McConnellin kirjasta Code Complete (s. 473):

// if allocation flag is zero
if( AllocFlag == 0 ) ...

Yllä oleva kommentti on täysin turha, koska saman asian voi lukea koodista. Kommenttia voi yrittää parantaa seuraavasti:

// if allocating new member
if( AllocFlag == 0 ) ...

Parempi tapa olisi kuitenkin käyttää nimettyä vakiota nollan sijasta:

// if allocating new member
if( AllocFlag == NEW_MEMBER ) ...

Toisaalta nyt kommentti tulee taas turhaksi (mikä on nyt hyvä asia, ja kommentti voidaan poistaa).

Koodia kommentoidessaan on hyvä pysähtyä miettimään, onko kommentista hyötyä. Toisaalta tässä vaiheessa, kun ohjelmointia vasta opetellaan, on hyvä kommentoida liikaa kuin liian vähän.

Ohjelmassa esiintyvät funktiot tulee kommentoida. Erityisesti jos kyseessä on luokan jäsenfunktio, kommentti kirjoitetaan otsikkotiedostoon (.hh) funktion yläpuolelle. Kommentista tulee selvitä, mitä funktio tekee, mitkä ovat sen parametrien ja mahdollisen paluuarvon merkitykset sekä mahdolliset arvot.

Vastaavasti myös kunkin tiedoston alussa tulee olla koko tiedostoa koskeva kommentti. Usein myös kontrollirakennetta on hyvä edeltää kommentti, jossa kerrotaan kontrollirakenteen tarkoitus.

Koodin asettelu

Vaikka C++ ei vaadikaan sisennyksiä, niitä tulee käyttää luettavuuden parantamiseksi.

Kontrollirakenteissa käytetään koodilohkon alku- ja loppusulkuja (eli aaltosulkuja: { }) aina, vaikka kyseisen kontrollirakenteen rungossa olisi vain yksi lause. Koodilohkon alku- ja loppusulut voi sijoittaa omille riveilleen, tai alkusulun voi kirjoittaa edellisen rivin loppuun. Voit käyttää kumpaa tahansa tapaa, kunhan käytät samaa tapaa kautta koko ohjelman.

Olio-ohjelmointi

Olio-ohjelmoinnissa kannattaa ihan ensimmäiseksi miettiä, onko kirjoittamasi luokka hyödyllinen ja järkevä. Luokan oleellinen tehtävä on kiinnittää data (attribuutit) ja sitä käsittelevä koodi (metodit) yhteen.

Jos luokassa ei ole yhtään metodia (lukuunottamatta rakentajaa ja purkajaa), luokka ei ole järkevä, koska silloin voisit yksinkertaisemmin käyttää tietuetta (struct). Myös jos luokassa on rakentajan ja purkajan lisäksi vain set- ja get-metodeita, niin silloinkin voisit käyttää tietuetta, jolloin näitäkään metodeita ei tarvittaisi. Jos taas luokassa ei ole yhtään attribuuttia, niin sen metodit eivät todennäköisesti liity yhteen (koska niillä ei ole mitään yhteistä dataa käsiteltävänä), jolloin niitä ei ole järkevää kirjoittaa luokkaan.

Tämän ja seuraavan kohdan tyyliseikat on valikoitu Matti Rintalan ja Jyke Jokisen kirjasta Olioiden ohjelmointi C++:lla (ss. 341-353). Useimmat niistä on mainittu ja selitetty laajemmin muualla materiaalissa, joten tässä ne vain luetellaan lyhyesti.

  • Yhdessä otsikkotiedostossa (.hh) on yksi julkinen rajapinta (luokka). Otsikkotiedosto sisältää vain esittelyitä (ei toteutuksia).

  • Otsikkotiedosto on suojattava moninkertaiselta käyttöönotolta:

    #ifndef CLASS_A_HH
    #define CLASS_A_HH
    ...
    #endif // CLASS_A_HH
    
  • include-direktiivillä saa ottaa käyttöön vain otsikkotiedostoja. Jos otetaan käyttöön useita tiedostoja, ensin luetellaan omat otsikkotiedostot ja sen jälkeen kirjaston tiedostot.

  • Rajapinnan toteutukset esitetään toteutustiedostossa (.cpp) samassa järjestyksessä kuin ne ovat vastaavassa otsikkotiedostossa (.hh).

  • Luokalle on määritelty hyvä julkinen rajapinta (mahdollisimman pieni), ja kaikki operaatiot tapahtuvat tämän rajapinnan kautta.

    • Erityisesti olio ei saa muuttaa toisen olion attribuuttien arvoja, ei vaikka oliot olisivat saman luokan ilmentymiä.
  • Luokan public-osa kirjoitetaan ennen private-osaa.

  • Luokan public-osassa ei ole jäsenmuuttujia.

  • Luokan private-osassa ei ole tarpeettomia muuttujia.

  • Luokan operaatioita ei tehdä jäsenfunktioiden ulkopuolella.

  • Luokan jäsenmuuttujat alustetaan rakentajan alustuslistassa, jossa ne luetellaan siinä järjestyksessä kuin ne on esitelty otsikkotiedostossa.

  • Jäsenfunktiosta tehdään vakiojäsenfunktio (const-funktio) aina kun mahdollista.

Muita tyyliseikkoja

Lopuksi luetellaan lyhyesti vielä joitakin yleisiä tyylisääntöjä.

  • Muuttujien näkyvyysalue tulee suunnitella mahdollisimman pieneksi (esimerkiksi koodilohko, funktio, luokka).

  • Jokainen muuttuja on määriteltävä eri lauseessa. Vältä siis muotoa int a, b; olevia esittelyitä.

  • Ohjelmassa ei saa olla alustamattomia muuttujia.

  • Osoittimen ja viitteen merkit kirjoitetaan heti tyypin nimen perään ilman välilyöntiä. Esimerkiksi

    Date* ptr = nullptr;
    void printDate( const Date& dateObject );
    
  • Funktion main paluuarvo on aina int.

  • Funktion parametrien nimet on annettava sekä esittelyssä että määrittelyssä, ja niiden on oltava molemmissa samat.

  • Funktion olioparametrin välitykseen käytetään viitettä aina kun mahdollista.

  • Funktion mahdolliset oletusarvoiset parametrit ovat näkyvissä esittelyssä, eikä niitä saa lisätä määrittelyssä.

  • Funktio ei saa palauttaa osoitinta tai viitettä sen omaan paikalliseen dataan.

  • Tietueella (struct) ei saa olla jäsenfunktioita, ainoastaan rakentajat, (tyhjä) purkaja sekä operaattorifunktiot kuitenkin sallitaan.

  • Älä indeksoi vektoreita hakasuluilla, vaan käytä operaatiota at. (Sama pätee muillekin säiliöille, joille kyseiset operaatiot on määritelty.)

  • Vektorin alkioiden pitää olla tietenkin samaa tyyppiä, mutta alkioiden pitää olla myös merkitykseltään samanlaisia. Jos esimerkiksi pitäisi tallettaa henkilön etunimi, sukunimi ja osoite (merkkijonoina), ei kannata käyttää taulukkoa vaan tietuetta (struct).

  • Vältä turhaa monimutkaisuutta. Esimerkiksi totuusarvoisen lausekkeen arvoa ei yleensä tarvitse tutkia, vaan kyseinen totuusarvo voidaan palauttaa suoraan. Konkreettisena esimerkkinä funktio

    bool is_negative(int number)
    {
        if(number < 0) {
            return true;
        } else {
            return false;
        }
    }
    

    voidaan kirjoittaa yksinkertaisemmin:

    bool is_negative(int number)
    {
        return number < 0;
    }
    

    (jolloin tässä tapauksessa herää kysymys, onko funktio edes tarpeellinen.)

  • Ellet ole ehdottoman varma, että osaat käyttää C++:n poikkeusmekanismia, niin älä yritä käyttää sitä. C++:n poikkeusten käsittelya ei opeteta tällä kurssilla. Erityisesti C++:n poikkeusmekanismia ei pidä käyttää kontrollirakenteiden sijasta (kuten olet ehkä tehnyt Python-ohjelmissa).

C++:ssa on muitakin tyylisääntöjä, jotka liittyvät esimerkiksi dynaamiseen muistinhallintaan ja luokkien periytymiseen. Näihin palataaan kyseisten aiheiden kohdalla.

Toivomus

Kurssin koodipohjissa ja esimerkeissä olemme pyrkineet noudattamaan yllä kerrottuja tyylisääntöjä. Aina emme ole onnistuneet, sillä koodeja ovat kirjoittaneet useat eri henkilöt usein kovassa kiireessä. Toivomme, että ilmiannat tyylisääntöjen rikkeet.