STL-kirjasto

Tieto vs. kontrolli (struct ja enum)


Tässä luvussa tutustutaan tieto-ohjattuun ohjelmointiin. Ensin tarvitaan kaksi C++:n tietotyyppiä: tietueet (struct) ja luettelotyypit (enum).

Tietuetyyppi (struct)

Monissa ohjelmointikielissä ohjelmoija voi määritellä omia tietotyyppejä, joiden avulla yhteen kokonaisuuteen kootaan keskenään erityyppisiä tietoja. Tällainen tietotyyppi on tietue (record); C++:ssa se määritellään varatulla sanalla struct. Tietue kokoaa kenttiä yhteen, mutta tässä vaiheessa kenttiä käsitellään suoraan eikä tyypillä ole omaa funktiorajapintaa.

#include <string>

using namespace std;

struct Tuote {
    string tuotenimi;
    double hinta;
};

Määrittelyn jälkeen Tuote on uusi tietotyyppi. Sen muuttujan voi alustaa ja kenttiin pääsee pisteoperaattorilla:

Tuote tavara = { "saippua", 1.23 };
tavara.hinta = 0.9 * tavara.hinta;  // 10 % alennus
cout << tavara.tuotenimi << ": " << tavara.hinta << endl;
tavara = { "appelsiini", 0.45 };

Tietuetta voi käyttää myös vektorin alkion tyyppinä, funktion parametri- tai paluuarvona sekä tavalliseen tapaan arvo- tai viiteparametrina.

Tietue sopii esimerkiksi henkilön tietojen kuvaamiseen:

struct Henkilo {
    string etunimi;
    string sukunimi;
    string osoite;
    string sahkoposti;
};

Nämä neljä kenttää ovat kaikki merkkijonoja, mutta nelialkioinen vektori ei silti olisi paras rakenne. Tietueen nimetyt kentät kertovat, mitä kukin arvo tarkoittaa. Vektorin indeksit eivät sitä kerro. Älä siis kokoa merkitykseltään erilaisia tietoja samaan vektoriin vain siksi, että niiden tyyppi on sama.

Luettelotyyppi (enum class)

Jos muuttujalla voi olla vain muutama tietty sallittu arvo, tavallinen string tai int sallisi paljon virheellisiä vaihtoehtoja. Esimerkiksi kirjan tila voi olla hyllyssä, lainassa, varattu tai hukassa. Luettelotyypissä ohjelmoija määrittelee sallitut arvot etukäteen.

enum class Kirjantila { HYLLYSSA, LAINASSA, VARATTU, HUKASSA };

Kirjantila tila = Kirjantila::HYLLYSSA;

Arvon yhteydessä käytetään tyypin nimeä tarkentimena, kuten Kirjantila::HYLLYSSA. Tämä erottaa luettelon arvot toisistaan ja auttaa kääntäjää havaitsemaan virheellisiä arvoja.

Luettelo sopii myös toisiinsa liittyville vakioille. Erillisten kokonaislukuvakioiden sijaan:

const int PUNAINEN = 0;
const int KELTAINEN = 1;
const int VIHREA = 2;

voit määritellä oman tyypin:

enum class LiikenneValo { PUNAINEN, KELTAINEN, VIHREA };

Luettelotyypin arvojen läpikäynti

C++ antaa luettelon arvoille taustalla kokonaislukuarvot, jotka alkavat oletuksena nollasta. Muunnosta luettelotyypin ja kokonaisluvun välillä ei kuitenkaan tehdä automaattisesti. Jos muunnosta tarvitaan, se tehdään näkyvästi static_cast-operaatiolla:

#include <iostream>

using namespace std;

enum class Viikonpaiva { MA, TI, KE, TO, PE, LA, SU };
const int PAIVIA_VIIKOSSA = static_cast<int>(Viikonpaiva::SU) + 1;

Viikonpaiva seuraava(Viikonpaiva paiva) {
    return static_cast<Viikonpaiva>(
        (static_cast<int>(paiva) + 1) % PAIVIA_VIIKOSSA);
}

Viikonpaiva::MA saa arvon 0, TI arvon 1 ja niin edelleen. Siksi viikon päivien lukumäärä saadaan viimeisen arvon numerosta lisäämällä siihen yksi. Funktio seuraava kierrättää viimeisen päivän jälkeen takaisin maanantaihin jakojäännösoperaattorin avulla.

Luettelon arvoja voi käydä läpi niiden järjestyksessä. Alla oleva ohjelma tulostaa viikonpäivälle viestin ja päättää silmukan sunnuntain jälkeen:

for (Viikonpaiva paiva = Viikonpaiva::MA;
     paiva <= Viikonpaiva::SU;
     paiva = seuraava(paiva)) {
    cout << "Paivan numero "
         << static_cast<int>(paiva) << ": ";

    if (paiva == Viikonpaiva::MA) {
        cout << "Maanantai :( :(" << endl;
    } else if (paiva == Viikonpaiva::TI) {
        cout << "Tiistai :(" << endl;
    } else if (paiva == Viikonpaiva::KE) {
        cout << "Keskiviikko :(" << endl;
    } else if (paiva == Viikonpaiva::TO) {
        cout << "Torstai :|" << endl;
    } else if (paiva == Viikonpaiva::PE) {
        cout << "Perjantai :)" << endl;
    } else if (paiva == Viikonpaiva::LA) {
        cout << "Lauantai :) :)" << endl;
    } else if (paiva == Viikonpaiva::SU) {
        cout << "Sunnuntai :|" << endl;
        break;
    }
}

Vertailuehto paiva <= Viikonpaiva::SU on tässä ylimääräinen, koska seuraava kierrättää sunnuntain jälkeen takaisin maanantaihin. Silmukka päättyy break-käskyyn. Syklisessä arvojoukossa yksinkertaisempi ikuinen silmukka ja selkeä lopetusehto voivat olla luettavampi ratkaisu.

Saman kierroksen voi toteuttaa myös while-silmukalla:

Viikonpaiva paiva = Viikonpaiva::MA;
while (true) {
    // Tulosta ja käsittele tämänhetkinen viikonpäivä.
    paiva = seuraava(paiva);
    if (paiva == Viikonpaiva::MA) {
        break;  // Koko kierros on tehty.
    }
}

Jos halutaan käsitellä vain valitut päivät, rajoitettu for-silmukka käy läpi alustuslistan arvot:

for (Viikonpaiva paiva : { Viikonpaiva::KE,
                           Viikonpaiva::MA,
                           Viikonpaiva::PE }) {
    cout << "Viikonpaivan numero on "
         << static_cast<int>(paiva) << endl;
}

Tieto-ohjattu ohjelmointi

Ohjelmat koostuvat tiedosta (data) ja käskyistä, joilla tietoa käsitellään. Raja niiden välillä ei ole aina selvä: joskus sopivasti järjestetty tieto voi korvata suuren joukon erillisiä käskyjä. Tätä ajatusta hyödyntää tieto-ohjattu ohjelmointi (data-driven tai data-directed programming).

Tieto-ohjatussa ratkaisussa suunnitellaan tietorakenne, johon lähtötiedot ja niihin liittyvät tulokset tallennetaan. Yleiskäyttöinen käsittelykoodi käy tiedot läpi sen sijaan, että jokaista tapausta varten kirjoitettaisiin oma käskyhaara. Seuraavan esimerkin koko ohjelma löytyy hakemistosta examples/03/datadrivenprogramming.

Oletetaan, että haluamme muuntaa Yhdysvaltain osavaltion nimen lyhenteeksi. Pitkä if-ketju toimii, mutta jokainen osavaltio vaatii oman ehtonsa:

#include <string>

using namespace std;

enum class PostalAbbr { AL, AK, AZ, AR, CA, CO, ERROR_CODE };

PostalAbbr name_to_abbreviation(const string& name) {
    if (name == "Alabama") {
        return PostalAbbr::AL;
    } else if (name == "Alaska") {
        return PostalAbbr::AK;
    } else if (name == "Arizona") {
        return PostalAbbr::AZ;
    } // ... vastaavat haarat muille osavaltioille
    else {
        return PostalAbbr::ERROR_CODE;
    }
}

Kun tapaukset tallennetaan tietueita sisältävään vektoriin, käsittelyfunktio voi pysyä lyhyenä:

#include <string>
#include <vector>

using namespace std;

struct StateInfo {
    string name;
    PostalAbbr abbreviation;
};

const vector<StateInfo> STATES = {
    { "Alabama", PostalAbbr::AL },
    { "Alaska", PostalAbbr::AK },
    { "Arizona", PostalAbbr::AZ },
    { "Arkansas", PostalAbbr::AR },
    { "California", PostalAbbr::CA },
    { "Colorado", PostalAbbr::CO }
    // ... muut osavaltiot
};

PostalAbbr name_to_abbreviation(const string& name) {
    for (const auto& state : STATES) {
        if (name == state.name) {
            return state.abbreviation;
        }
    }
    return PostalAbbr::ERROR_CODE;
}

Kun osavaltioiden tiedot tallennetaan rakenteisesti, itse hakualgoritmi ei tarvitse omaa if-haaraa jokaiselle osavaltiolle. Tieto korvaa toistuvia käskyjä: uuden osavaltion lisääminen tarkoittaa uuden tietorivin lisäämistä, ei uuden koodilohkon kirjoittamista.

Tieto-ohjattu ohjelmointi tekee ohjelmista usein lyhyempiä ja selkeämpiä, vähentää virheiden mahdollisuutta sekä helpottaa laajentamista ja ylläpitoa. Hyvin pitkäksi kasvava if- tai switch-rakenne voi olla merkki siitä, että tapaukset kannattaisi tallentaa tietorakenteeseen.