Funktio-osoittimet

Usein ohjelmointikielistä puhuttaessa sanotaan, että ohjelmiin liittyy tietoa ja toimenpiteitä. Jako ei kuitenkaan ole näin suoraviivainen. Myös funktiot ovat tietoa.

Joissakin ohjelmointikielissä funktioita voi käsitellä samoin kuin muitakin tietoalkioita. Funktion voi esimerkiksi välittää parametrina toiselle funktiolle tai funktion paluuarvona tai tallentaa muuttujaan. C++:ssa tällainen onnistuu niin kutsutun funktio-osoittimen avulla.

Funktiota, joka ottaa parametrinaan toisen funktion tai palauttaa toisen funktion, kutsutaan korkeamman asteen funktioksi (higher-order function). Funktioita, jotka eivät ole korkeampaa astetta, voidaan kutsua ensimmäisen asteen funktioiksi (first-order function).

Numeerinen integrointi

Tutkitaan esimerkkiä, jonka löydät hakemistosta examples/09/integral.

Numeerisessa integroinnissa ideana on approksimoida funktion kuvaajan ja X-akselin väliin jäävän alueen pinta-alaa siten, että jaetaan alue kapeisiin suorakaiteen muotoisiin alueisiin ja lasketaan näiden suorakaiteen muotoisten alueiden pinta-alat yhteen.

Funktion kuvaajan ja X-akselin välisen alueen pinta-ala

Yllä olevassa kuvassa esitetään mustalla piirretyn kuvaajan ja X-akselin väliin jäävän alueen pinta-alan approksimointia kahdella eri tavalla. Vihreäksi väritettyjen suorakaiteiden pinta-alat yhteen laskemalla saataisiin alasumma pinta-alan likiarvolle. Keltaiset suorakaiteet ovat osittain piilossa vihreiden suorakaiteiden takana, mutta laskemalla niiden pinta-alat yhteen, saataisiin puolestaan yläsumma samalle pinta-alan likiarvolle. Likiarvoa saataisiin tarkennettua suorakaiteiden lukumäärää kasvattamalla (eli siis niiden leveyttä pienentämällä).

Esimerkkiohjelmassamme ei lasketa alasummaa eikä yläsummaa, vaan suorakaiteen korkeus määräytyy aina sen mukaan, mikä funktion arvo suorakaiteen keskikohdassa on. Kuvassa nämä pisteet on merkitty sinisellä pallukalla.

Ohjelmaesimerkissä on toteutettu funktio integrate, joka saa ensimmäisenä parametrinaan integroitavan funktion f:

double integrate(Func f, double left, double right, int number_of_partitions = 500);

Funktion toteutuksessa on käytetty apuna määrittelyä

using Func = decltype(&polynomial);

Käskyllä decltype saadaan selville minkä tahansa muuttujan tai funktion tietotyyppi. Yhtä hyvin oltaisiin voitu määritellä C++:n tyyppimäärittelysyntaksilla:

using Func = double(*)(double);

joka tarkoittaa, että Func on osoitin funktioon, joka palauttaa doublen ja saa parametrinaan doublen. Määrittelyssä ensimmäinen double on paluuarvo, (*) tarkoittaa funktio-osoitinta ja sulkujen sisällä on parametriluettelo.

Tietenkin olisi mahdollista myös jättää using-lause kokonaan pois, ja määritellä funktio integrate seuraavasti:

double integrate(double(*f)(double), double left, double right, int number_of_partitions = 500);

Huomaa, että edellä ei siis vain korvata tunnistetta Func sillä, mitä using-lauseessa luki sijoitusoperaattorin oikealla puolella. Kiinnitä huomiosi siihen, missä kohdassa funktio-osoittimen nimi f lukee.

Kun funktiota integrate kutsutaan, pitää sille antaa parametrina osoitin funktioon, esimerkiksi:

cout << integrate(sin,        0,  2) << endl;
cout << integrate(cos,        0,  2) << endl;
cout << integrate(sqrt,       0,  2) << endl;
cout << integrate(polynomial, 0,  2) << endl;

Huomaa, että näissä ei ole sulkeita ensimmäisen parametrin perässä, koska kyse ei ole funktiokutsusta vaan osoittimesta funktioon.

Funktio-osoittimien käyttötarkoituksia

Palaamme funktio-osoittimiin seuraavien kierrosten aikana. Funktio-osoittimista on hyötyä esimerkiksi seuraavissa tilanteissa:

  • Haluamme valita, mitä toiminnallisuutta ohjelmassa suoritetaan, mutta emme halua toteuttaa ohjelmaan valtavaa if-rakennetta, jonka jokaisessa lohkossa on yksi valittavana olevista toiminnallisuuksista (vrt. esimerkki examples/04/datadrivenprogramming ja materiaalin kohta Tieto-ohjattu ohjelmointi 4-kierrokselta).
  • Toteutamme graafisia käyttöliittymiä ja haluamme sitoa jonkin toiminnon johonkin käyttöliittymäkomponenttiin.