Standardi C++

Tosoittimet jäsenfunktioihin

Onko ”osoitin jäsenfunktioon” eri tyyppi kuin ”osoitin funktioon”?

Jep.

Harkitse seuraavaa funktiota:

int f(char a, float b);

Tämän funktion tyyppi on erilainen riippuen siitä, onko se tavallinen funktio vai jonkin luokan ei-staticstatic-jäsenfunktio:

Huomaa: jos se on static-jäsenfunktio class Fred-luokassa class Fred, niin sen tyyppi on sama, kuin jos kyse olisi tavallisesta funktiosta:int (*)(char,float).

Miten välitän osoittimen jäsenfunktioon signaalinkäsittelijälle, X-tapahtumakutsulle, järjestelmäkutsulle, joka käynnistää säikeen/tehtävän jne.

Ei saa.

Koska jäsenfunktio on merkityksetön ilman objektia, johon sitä voi kutsua, tätä ei voi tehdä suoraan (jos The X WindowSystem kirjoitettaisiin uudelleen C++:lla, se luultavasti välittäisi viitteitä objekteihin, ei vain osoittimia funktioihin;luonnollisesti objektit ilmentäisivät vaadittua funktiota ja luultavasti paljon muuta).

Parannuksena olemassa oleviin ohjelmistoihin, käytä ylimmän tason (ei-jäsen) funktiota wrapperina, joka ottaa objektin, joka on saatu jollakin muulla tekniikalla. Riippuen kutsumastasi rutiinista, tämä ”muu tekniikka” voi olla triviaali tai se voi vaatia sinulta hieman työtä. Järjestelmäkutsu, joka käynnistää säikeen, saattaa esimerkiksi vaatia, että välität toiminnon osoittimen yhdessä void*:n kanssa, joten voit välittää objektin osoittimen void*:ssä. Monet reaaliaikaiset käyttöjärjestelmät tekevät jotakin vastaavaa funktiolle, joka käynnistää uuden tehtävän. Pahimmassa tapauksessa voit tallentaa objektin osoittimen globaaliin muuttujaan; tämä saattaa olla tarpeen Unixin signaalinkäsittelijöissä (mutta globaalit muuttujat eivät yleensä ole toivottuja). Joka tapauksessa ylimmän tason funktio kutsuisi objektin haluttua jäsenfunktiota.

Tässä on esimerkki pahimmasta tapauksesta (käyttämällä globaalia). Oletetaan, että haluat kutsua Fred::memberFn() keskeytyksellä:

Huomautus: static-jäsenfunktiot eivät vaadi varsinaista objektia kutsuttavaksi, sopointeritstatic-jäsenfunktioihin ovat yleensä tyypiltään yhteensopivia tavallisten osoittimien funktioihin kanssa. Kuitenkin, vaikka se luultavasti toimii useimmissa kääntäjissä, sen pitäisi itse asiassa olla extern "C"-ei-jäsenfunktio ollakseen oikea, koska ”C-linkitys” ei kata vain nimien mankeloinnin kaltaisia asioita, vaan myös kutsukäytännöt, jotka saattavat olla erilaisia C:n ja C++:n välillä.

Miksi saan jatkuvasti kääntämisvirheitä (type mismatch), kun yritän käyttää jäsenfunktiota keskeytyspalvelurutiinina?

Tämä on erikoistapaus kahdesta edellisestä kysymyksestä, joten lue ensin kaksi edellistä vastausta.

Eistaticstatic-jäsenfunktioilla on piiloparametri, joka vastaa this-osoitinta. this-osoitin osoittaa objektin instanssitietoihin. Järjestelmän keskeytyslaitteisto/-firmware ei kykene tarjoamaan this-osoitinargumenttia. Sinun on käytettävä ”tavallisia” funktioita (muita kuin luokan jäseniä) tai static-jäsenfunktioita keskeytyspalvelurutiineina.

Yksi mahdollinen ratkaisu on käyttää static-jäsenfunktiota keskeytyspalvelurutiinina ja antaa kyseisen funktion etsiä jostain instanssi/jäsenpari, jota pitäisi kutsua keskeytyksen yhteydessä. Näin saadaan aikaan se, että jäsenfunktio kutsutaan keskeytyksen yhteydessä, mutta teknisistä syistä sinun on ensin kutsuttava välifunktiota.

Miksi minulla on ongelmia C++-funktion osoitteen ottamisessa?

Lyhyt vastaus: Jos yrität tallentaa sen (tai välittää sen) osoittimena funktiolle, niin siinä on ongelma – tämä on seuraus edellisestä UKK:sta.

Pitkä vastaus: C++:ssa jäsenfunktioilla on implisiittinen parametri, joka osoittaa objektiin (this osoitin jäsenfunktion sisällä). Tavallisilla C-funktioilla voidaan ajatella olevan erilainen kutsukäytäntö kuin jäsenfunktioilla, joten niiden osoittimien tyypit (osoitin jäsenfunktiolle vs. osoitin funktiolle) ovat erilaisia ja yhteensopimattomia. C++ ottaa käyttöön uuden osoitintyypin, jota kutsutaan nimellä osoitin jäsenelle, jota voidaan kutsua vain antamalla objekti.

Huomautus: älä yritä ”heittää” osoitin jäsenelle -funktiota osoitin funktiolle -funktioksi; lopputulos on määrittelemätön ja luultavasti katastrofaalinen. Esim. osoittimen jäsenfunktioon ei tarvitse sisältää sopivan funktion koneosoitetta. Kuten edellisessä esimerkissä sanottiin, jos sinulla on osoitin tavalliseen C-funktioon, käytä joko atop-tason (ei-jäsen) funktiota tai static (luokan) jäsenfunktiota.

Miten voin välttää syntaksivirheitä luodessani osoittimia jäseniin?

Käyttäkää typedef:aa.

Jaaha, joo joo, tiedänhän minä sen: olet erilainen. Olet fiksu. Osaat tehdä nämä asiat ilman typedef. Huokaus. Olen saanut monia sähköpostiviestejä ihmisiltä, jotka sinun laillasi kieltäytyivät noudattamasta tämän FAQ:n yksinkertaisia neuvoja. He tuhlasivat tuntikausia aikaansa, kun 10 sekunnin typedef olisi yksinkertaistanut heidän elämäänsä. Lisäksi on myönnettävä, ettet kirjoita koodia, jota vain sinä voit lukea; toivottavasti kirjoitat koodia, jota muutkin voivat lukea – kun he ovat väsyneitä – kun heillä on omat määräaikansa ja omat haasteensa. Miksi siis tarkoituksella tehdä elämästä vaikeampaa itsellesi ja muille? Ole fiksu: käytä typedef.

Tässä on esimerkkiluokka:

class Fred {public: int f(char x, float y); int g(char x, float y); int h(char x, float y); int i(char x, float y); // ...};

Typedef on triviaali:

typedef int (Fred::*FredMemFn)(char x, float y); // Please do this!

Se on siinä! FredMemFn on tyypin nimi, ja tämän tyypin osoitin osoittaa mihin tahansa Fred:n jäseneen, joka ottaa (char,float), kuten Fred:n f, g, h ja i.

Tällöin on triviaalia julistaa jäsenfunktio-osoitin:

int main(){ FredMemFn p = &Fred::f; // ...}

Ja on myös triviaalia julistaa funktioita, jotka vastaanottavat jäsenfunktio-osoittimia:

void userCode(FredMemFn p){ /*...*/ }

Ja on myös triviaalia julistaa funktioita, jotka palauttavat jäsenfunktio-osoittimia:

FredMemFn userCode(){ /*...*/ }

Käyttäkää siis mieluummin typedef. Joko niin tai älä lähetä minulle sähköpostia ongelmista, joita sinulla on jäsenfunktio-osoittimien kanssa!

Miten voin välttää syntaksivirheitä kutsuessani jäsenfunktiota jäsenfunktio-osoittimella?

Jos sinulla on käytössäsi kääntäjä ja standardikirjasto, joka toteuttaa tulevan C++17-standardin asianmukaiset osat, käytä std::invoke. Muussa tapauksessa käytä #define-makroa.

Kiltti.

Kiltti.

Saan aivan liikaa sähköposteja hämmentyneiltä ihmisiltä, jotka kieltäytyivät noudattamasta tätä neuvoa. Se on niin yksinkertaista. Tiedän, että et tarvitsestd::invoke tai makroa, ja asiantuntija, jonka kanssa puhuit, voi tehdä sen ilman kumpaakaan niistä, mutta älä anna egosi tulla sen tielle, mikä on tärkeää: raha. Muiden ohjelmoijien on luettava/ylläpidettävä koodiasi. Kyllä, tiedän: olet fiksumpi kuin kaikki muut; hienoa. Ja olet mahtava; hienoa. Mutta älä lisää tarpeetonta monimutkaisuutta koodiisi.

std::invoke käyttäminen on triviaalia. Huomaa: FredMemFn on typedef osoitin jäsenelle -tyyppiä varten:

Jos et voi käyttää std::invoke, vähennä ylläpitokustannuksia käyttämällä paradoksaalisesti #define-makroa tässä erityistapauksessa.

(Normaalisti en pidä #define-makroista, mutta niitä kannattaa käyttää osoittimien tomembers kanssa, koska ne parantavat tällaisen koodin luettavuutta ja kirjoitettavuutta.)

Makro on triviaali:

#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))

Makron käyttö on myös triviaalia. Huomaa: FredMemFn on typedef osoitin jäsentyyppiin:

Syy, miksi std::invoke tai tämä makro on hyvä idea, on se, että jäsenfunktioiden kutsut ovat usein paljon monimutkaisempia kuin äsken annettu yksinkertainen esimerkki. Ero luettavuudessa ja kirjoitettavuudessa on merkittävä.comp.lang.c++ on joutunut kestämään satoja ja taas satoja viestejä hämmentyneiltä ohjelmoijilta, jotka eivät ole oikein ymmärtäneet syntaksia. Lähes kaikki nämä virheet olisivat kadonneet, jos he olisivat käyttäneet std::invoke tai edellä mainittua makroa.

Huomautus: #define-makrot ovat pahoja neljällä eri tavalla: paha#1,paha#2, paha#3 ja paha#4. Mutta ne ovat siltihyödyllisiä joskus. Mutta sinun pitäisi silti tuntea epämääräistä häpeän tunnetta niiden käytön jälkeen.

Miten luon ja käytän osoittaja-jäsen-funktio -matriisia?

Käytä sekä typedef että std::invoke tai aiemmin kuvattua #define-makroa, ja olet 90-prosenttisesti valmis.

Vaihe 1: luo typedef:

Vaihe 2: luo #define-makro, jos sinulla ei ole std::invoke:

#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))

Makroa #define, jos sinulla ei ole std::invoke:

#define CALL_MEMBER_FN(object,ptrToMember) ((object).*(ptrToMember))

Nyt joukostasi pointers-to-member-functions on suoraviivainen:

FredMemFn a = { &Fred::f, &Fred::g, &Fred::h, &Fred::i };

Ja yhden jäsenfunktio-osoittimen käyttösi on myös suoraviivaista:

void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: std::invoke(a, fred, 'x', 3.14);}

tai jos sinulla ei ole std::invoke,

void userCode(Fred& fred, int memFnNum){ // Assume memFnNum is between 0 and 3 inclusive: CALL_MEMBER_FN(fred, a) ('x', 3.14);}

Huomautus: #define-makrot ovat pahoja neljällä eri tavalla: paha#1,paha#2, paha#3, paha#4. Mutta ne ovat siltihyödyllisiä joskus. Häpeä, tunne syyllisyyttä, mutta kun paha konstruktio, kuten makro, parantaa ohjelmistoasi, käytä sitä.

Miten deklarioin osoitin jäsenfunktioon, joka osoittaa const-jäsenfunktioon?

Lyhyt vastaus: lisää const ):n oikealle puolelle, kun käytät typedef:aa jäsenfunktio-osoitintyypin deklaraatioon.

Yksi esimerkki: Oletetaan, että haluat jäsenfunktion osoittimen, joka osoittaa Fred::f, Fred::g tai Fred::h:

class Fred {public: int f(int i) const; int g(int i) const; int h(int j) const; // ...};

Tällöin kun käytät typedef:aa ilmoittaaksesi jäsenfunktio-osoitin-tyypin, sen pitäisi näyttää tältä:

Juuri noin!

Silloin voit julistaa/luovuttaa/palauttaa jäsenfunktio-osoittimia aivan kuten normaalisti:

Mitä eroa on operaattoreilla .* ja ->*?

Sinun ei tarvitse ymmärtää tätä, jos käytät std::invoke:tä tai makroa jäsenfunktio-osoitin-kutsuihin. Ohyea, käytä tässä tapauksessa std::invoke tai makroa. Ja mainitsinko jo, että sinun pitäisi käyttää std::invoke tai makroa tässä tapauksessa?!!?

Esimerkiksi:

MUTTA harkitse sen sijaan std::invoke tai makron käyttämistä:

void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}

tai

Mutta harkitse sen sijaan std::invoke tai makron käyttämistä:

void sample(Fred x, Fred& y, Fred* z, FredMemFn func){ std::invoke(func, x, 42, 3.14); std::invoke(func, y, 42, 3.14); std::invoke(func, *z, 42, 3.14);}

tai

Mutta, kuten aiemmin käsiteltiin, reaalimaailmassa esiintyvät kehotukset ovat usein paljon mutkikkaammat kuin tässä olevat simpleonit, joten std::invoke:n tai makron käyttäminen tyypillisesti parantavat ohjelmakoodin kirjoitusominaisuuksia ja helppolukuisuuttatai.

Voinko muuntaa osoittimen jäsenfunktioon void*:ksi?

Ei!

Teknisiä yksityiskohtia: Osoittimet jäsenfunktioihin ja osoittimet dataan eivät välttämättä esitetä samalla tavalla. Osoitin jäsenfunktioon saattaa olla datarakenne eikä yksittäinen osoitin. Ajattele asiaa: jos se osoittaa virtuaalifunktioon, se ei välttämättä itse asiassa osoita staattisesti resolvoitavaan koodikasaan, joten se ei välttämättä ole edes normaali osoite – se saattaa olla jonkinlainen erilainen tietorakenne.

Älä lähetä minulle sähköpostia, jos ylläoleva näyttää toimivan tietyssä versiossasi tietyllä kääntäjäsi tietyllä käyttöjärjestelmälläsi. Minua ei kiinnosta. Se on laitonta, piste.

Voinko muuntaa osoittimen funktioon void*:ksi?

Ei!

Teknisiä yksityiskohtia: void* Osoittimet ovat osoittimia dataan, ja funktio-osoittimet osoittavat funktioihin. Kieli ei vaadi, että funktiot ja data ovat samassa osoiteavaruudessa, joten esimerkkinä, ei rajoituksena, arkkitehtuureissa, joissa ne ovat eri osoiteavaruuksissa, nämä kaksi eri osoitintyyppiä eivät ole vertailukelpoisia.

Älkää lähettäkö minulle sähköpostia, jos yllä oleva näyttää toimivan tietyssä versiossasi tietyssä kääntäjässä tietyssä käyttöjärjestelmässäsi. En välitä siitä. Se on laitonta, piste.

Tarvitsen jotain funktio-osoittimien kaltaista, mutta joustavammalla ja/tai säikeenkestävämmällä tavalla; onko muuta tapaa?

Käytä funktioidia.

Mikä hemmetti on funktioidi ja miksi käyttäisin sellaista?

Funktioidit ovat funktioita steroideilla. Funktionoidit ovat ehdottomasti tehokkaampia kuin funktiot, ja tämä ylimääräinen teho ratkaisee joitakin (ei kaikkia) niistä haasteista, joita tyypillisesti kohdataan käytettäessä funktio-osoittimia.

Tehdään esimerkki, jossa näytetään funktio-osoittimien perinteinen käyttö, ja sitten käännetään tämä esimerkki funktionoidien käyttöön. Perinteinen funktio-osoittimen idea on se, että sinulla on joukko yhteensopivia funktioita:

int funct1( /*...params...*/ ) { /*...code...*/ }int funct2( /*...params...*/ ) { /*...code...*/ }int funct3( /*...params...*/ ) { /*...code...*/ }

Sitten käytät niitä funktio-osoittimilla:

typedef int(*FunctPtr)( /*...params...*/ );void myCode(FunctPtr f){ // ... f( /*...args-go-here...*/ ); // ...}

Joskus ihmiset luovat näistä funktio-osoittimista joukon:

FunctPtr array;array = funct1;array = funct1;array = funct3;array = funct2;// ...

Tällöin he kutsuvat funktiota käyttämällä joukkoa:

array( /*...args-go-here...*/ );

Funktionoidien kanssa luodaan ensin perusluokka, jossa on puhtaasti virtuaalinen metodi:

Sitten kolmen funktion sijasta luodaan kolme johdettua luokkaa:

Sitten funktio-osoittimen välittämisen sijasta välitetään Funct*. Luon typedef:n nimeltä FunctPtr pelkästään jotta loppuosa koodista muistuttaisi vanhanaikaista lähestymistapaa:

typedef Funct* FunctPtr;void myCode(FunctPtr f){ // ... f->doit( /*...args-go-here...*/ ); // ...}

Voit luoda niistä joukon lähes samalla tavalla:

Tämä antaa meille ensimmäisen vihjeen siitä, missä funktioidit ovat ehdottomasti tehokkaampia kuin funktio-osoittimet: se, että funktioidi-lähestymistavalla on argumentteja, jotka voit välittää ctoreille (esitetty yllä muodossa …ctor-args…), kun taas funktio-osoitinversiossa ei ole. Ajattele functionoid-oliota pakastekuivattuna funktiokutsuna (painotus sanalla call). Toisin kuin osoitin funktioon, functionoidi on (käsitteellisesti) osoitin osittain kutsuttuun funktioon. Kuvittele hetkeksi teknologia, jonka avulla voit välittää funktiolle joitakin, mutta et kaikkia argumentteja, ja sitten voit jäädyttää ja kuivata tämän (osittain suoritetun) kutsun. Kuvitellaan, että tämä tekniikka antaa sinulle takaisin jonkinlaisen maagisen osoittimen tuohon jäädytettyyn, osittain valmiiseen funktiokutsuun. Myöhemmin annat loput argumentit käyttäen tätä osoitinta, ja järjestelmä ottaa maagisesti alkuperäiset (jäädytetyt) argumentit, yhdistää ne kaikkiin paikallisiin muuttujiin, jotka funktio laski ennen jäädytystä, yhdistää ne äskettäin annettuihin argumentteihin ja jatkaa funktion suoritusta siitä, mihin se jäi jäädytyksen jälkeen. Tämä saattaa kuulostaa tieteiskirjallisuudelta, mutta se on periaatteessa se, mitä funktioiden avulla voit tehdä. Lisäksi niiden avulla voit toistuvasti ”viimeistellä” tuon jäädytetyn funktiosuorittimen erilaisilla ”jäljellä olevilla parametreilla” niin usein kuin haluat. Lisäksi ne sallivat (eivät vaadi) sinun muuttaa pakastekuivattua tilaa, kun sitä kutsutaan, mikä tarkoittaa, että funktioidit voivat muistaa tietoa kutsusta toiseen.

Viedään jalat takaisin maan pinnalle ja tehdään pari esimerkkiä selittääksemme, mitä kaikki tämä hölynpöly oikeasti tarkoittaa.

Oletetaan, että alkuperäiset funktiot (vanhanaikaiseen funktio-osoitin-tyyliin) ottivat hieman erilaisia parametreja.

int funct1(int x, float y){ /*...code...*/ }int funct2(int x, const std::string& y, int z){ /*...code...*/ }int funct3(int x, const std::vector<double>& y){ /*...code...*/ }

Kun parametrit ovat erilaiset, vanhanaikaista funktio-osoitin-tyyliä on vaikea käyttää, koska kutsuja ei tiedä, mitä parametreja välittää (kutsujalla on vain osoitin funktioon, ei funktion nimeä tai,kun parametrit ovat erilaiset, sen parametrien lukumäärää ja tyyppejä) (älkää kirjoittako minulle sähköpostia tästä; joo, voitte tehdä sen, mutta joudutte seisomaan päänne päällä ja tekemään sotkuisia juttuja; älkää kuitenkaan kirjoittako minulle tästä – käyttäkää sen sijaan funktio-oideja).

Funktionoidien avulla tilanne on ainakin joskus paljon parempi. Koska funktioidi voidaan ajatella pakkaskuivaksi funktiokutsuksi, otetaan vain ei-yhteiset argsit, kuten ne, joita olen kutsunut y ja/tai z, ja tehdään niistäargsit vastaaville ctoreille. Voit myös siirtää yhteiset args (tässä tapauksessa int nimeltään x) ctoriin, mutta sinun ei tarvitse – sinulla on mahdollisuus siirtää ne puhtaasti virtuaaliseen doit()-metodiin sen sijaan. Oletan, että haluat siirtää x:n doit():een ja y:n ja/tai z:n ctoriin:

class Funct {public: virtual int doit(int x) = 0;};

Tällöin luot kolmen funktion sijaan kolme johdettua luokkaa:

Nyt huomaat, että ctorin parametrit jäädytetään funktioiden joukkoon, kun luot array of functionoids:

Siten kun käyttäjä kutsuu doit() yhteen näistä functionoidista, hän antaa ”jäljelle jääneet” argsit, ja kutsuyhdistää käsitteellisesti ctorille annetut alkuperäiset argsit doit()-metodiin annettujen argsien kanssa:

array->doit(12);

Kuten olen jo vihjannut, yksi funktioidien eduista on se, että sinulla voi olla useita instansseja esimerkiksi Funct1:stä, ja näihin instansseihin voi jäädyttää eri parametreja. Esimerkiksi array jaarray ovat molemmat tyyppiä Funct1, mutta array->doit(12):n käyttäytyminen on erilaista kuin array->doit(12):n, koska käyttäytyminen riippuu sekä 12:sta, joka välitettiin doit():lle, että ctorsille välitetyistä argeista.

Toinen funktioidien hyöty näkyy, jos muutamme esimerkin funktioidien joukosta paikalliseksi funktioidiksi. Alustukseksi palataan vanhanaikaiseen funktio-osoitin -lähestymistapaan ja kuvitellaan, että yrität välittää vertailufunktion sort() tai binarySearch() -rutiinille. sort()– tai binarySearch()rutiinin nimi on childRoutine() ja vertailufunktio-osoittimen tyyppi on FunctPtr:

void childRoutine(FunctPtr f){ // ... f( /*...args...*/ ); // ...}

Tällöin eri kutsujat välittäisivät erilaisia funktio-osoittimia sen mukaan, mikä heidän mielestään olisi paras:

void myCaller(){ // ... childRoutine(funct1); // ...}void yourCaller(){ // ... childRoutine(funct3); // ...}

Voidaan helposti kääntää tämä esimerkki funktio-oideja käyttävään esimerkkiin:

Tämän esimerkin taustaksi voimme nähdä funktio-oideista kaksi etua funktio-osoittimiin verrattuna. Edellä kuvattu ”ctor args”-hyöty sekä se, että funktioidit voivat ylläpitää tilaa kutsujen välillä säikeenkestävällä tavalla.Tavallisilla funktio-osoittimilla ihmiset yleensä ylläpitävät tilaa kutsujen välillä staattisen datan avulla. Staattinen data ei kuitenkaan ole varsinaisesti säikeenkestävää – staattinen data jaetaan kaikkien säikeiden kesken. Functionoid-lähestymistapa tarjoaa jotakin, joka on luonnostaan säikeenkestävää, koska koodi päätyy säikeen paikalliseen dataan. Toteutus on yksinkertainen: vaihda vanhanaikainen staattinen data instanssidatan jäseneksi functionoidin this-olion sisällä, ja kas, data ei ole vain säikeen paikallista, vaan se on jopa turvallista rekursiivisten kutsujen kanssa: jokaisella kutsulla yourCaller() on oma erillinen Funct3-olio, jolla on omat erilliset instanssidatansa.

Huomaa, että olemme voittaneet jotakin menettämättä mitään. Jos haluat säikeen globaalia dataa, funktiooidit voivat antaa youthat myös: vaihda se vain funktiooidin this-olion sisällä olevasta instanssidatan jäsenestä staattiseksi datan jäseneksi funktiooidin luokan sisällä, tai jopa paikallisen skaalan staattiseksi dataksi. Et olisi yhtään parempi kuin funktio-osoittimien kanssa, mutta et myöskään huonompi.

Funktionoidi antaa kolmannen vaihtoehdon, jota ei ole saatavilla vanhanaikaisella lähestymistavalla: funktionoidi antaa kutsujien päättää, haluavatko he säikeen paikallista vai säikeen globaalia dataa. He olisivat vastuussa käyttölockien käytöstä tapauksissa, joissa he haluaisivat thread-globaalia dataa, mutta ainakin heillä olisi mahdollisuus valita. Se on helppoa:

Funktionoidit eivät ratkaise kaikkia ongelmia, joita kohdataan joustavien ohjelmistojen tekemisessä, mutta ne ovat ehdottomasti tehokkaampia kuin funktio-osoittimet ja niitä kannattaa ainakin arvioida. Itse asiassa voit helposti todistaa, että funktioidit eivät menetä mitään valtaa funktio-osoittimiin nähden, koska voit kuvitella, että vanhanaikainen lähestymistapa funktio-osoittimiin onekvivalentti globaalin(!) funktioidiobjektin kanssa. Koska voit aina tehdä globaalin funktionoidiobjektin, et ole menettänyt mitään. QED.

Voidaanko funktioidit tehdä nopeammin kuin tavalliset funktiokutsut?

Kyllä.

Jos sinulla on pieni funktioidi, ja reaalimaailmassa se on melko yleistä, funktiokutsun kustannukset voivat olla korkeat verrattuna funktioidin tekemän työn kustannuksiin. Edellisessä FAQ:ssa funktioidit toteutettiin virtuaalifunktioiden avulla ja ne tyypillisesti maksavat funktiokutsun. Vaihtoehtoinen lähestymistapa käyttää malleja.

Seuraava esimerkki on hengeltään samanlainen kuin edellisen FAQ:n esimerkki. Olen nimennyt doit() uudelleen operator()():ksi parantaakseni kutsujan koodin luettavuutta ja antaakseni jollekulle mahdollisuuden välittää tavallisen funktio-osoittimen:

Ero tämän lähestymistavan ja edellisessä UKK:ssa esitetyn lähestymistavan välillä on se, että funktio-osoitin ”sidotaan ”kutsujaan kääntämisaikana eikä suoritusaikana. Ajattele sitä kuin parametrin välittämistä: jos tiedät jo kääntämisaikana, minkälaisen funktionoidin haluat lopulta välittää, voit käyttää edellä mainittua tekniikkaa, ja voit,ainakin tyypillisissä tapauksissa, saada nopeushyötyä siitä, että kääntäjälaajentaa funktionoidin koodin kutsujan sisällä. Tässä on esimerkki:

template <typename FunctObj>void myCode(FunctObj f){ // ... f( /*...args-go-here...*/ ); // ...}

Kun kääntäjä kääntää yllä olevan, se saattaa inline-laajentaa kutsun, mikä saattaa parantaa suorituskykyä.

Tässä on yksi tapa kutsua yllä olevaa:

void blah(){ // ... Funct2 x("functionoids are powerful", 42); myCode(x); // ...}

Sivutoiminto: kuten ensimmäisessä kappaleessa vihjattiin, voit myös välittää normaalien funktioiden nimet (tosin saattaisit joutua kärsimään funktiokutsun kustannuksista, kun kutsuja käyttää näitä):

void myNormalFunction(int x);void blah(){ // ... myCode(myNormalFunction); // ...}

Mitä eroa on funktioidilla ja funktiokutsun funktionaalisella funktiolla?

Funktioidi on objekti, jolla on yksi päämetodi. Se on periaatteessa C:n kaltaisen funktion, kuten printf(), OO-laajennus. Functionoidia käytetään aina, kun funktiolla on useampi kuin yksi sisäänmenopiste (eli useampi kuin yksi ”metodi”)ja/tai kun täytyy säilyttää tila kutsujen välillä säikeenkestävällä tavalla (C-tyylinen lähestymistapa tilan säilyttämiseen kutsujen välillä on lisätä funktioon paikallinen ”staattinen” muuttuja, mutta se on hirvittävän epävarmaa monisäikeisessä ympäristössä).

Funktori on functionoidin erikoistapaus: se on functionoidi, jonka metodi on ”funktiokutsuoperaattori” operator()(). Koska se ylikuormittaa funktiokutsuoperaattorin, koodi voi kutsua sen päämetodia käyttäen samaa syntaksia kuin funktiokutsussa. Jos esimerkiksi ”foo” on funktio, ”operator()()”-metodin kutsuminen ”foo”-objektiin tarkoittaa ”foo()”. Tästä on hyötyä malleissa, koska silloin mallissa voi olla malliparametri, jota käytetään funktiona, ja tämä parametri voi olla joko funktion nimi tai funktio-objekti. Siitä, että se on funktio-objekti, on suorituskykyetua, koska ”operator()()”-metodi voidaan rivittää (kun taas jos annat funktion osoitteen, sen on välttämättä oltava rivittämätön).

Tämä on erittäin hyödyllistä esimerkiksi lajiteltujen säiliöiden ”comparison”-funktiossa. C:ssä vertailufunktio välitetään aina osoittimella (esim, ks. allekirjoitus ”qsort()”), mutta C++:ssa parametri voi tulla joko osoittimena funktioon TAI funktio-objektin nimenä, ja tuloksena on, että lajitellut kontit C++:ssa voivat olla joissakin tapauksissa paljon nopeampia (eikä koskaan hitaampia) kuin vastaava C:ssä.

Koska Javassa ei ole mitään vastaavaa kuin malleissa, sen on käytettävä dynaamista sitomista kaikkiin näihin asioihin, ja dynaaminen sitominen tarkoittaa pakostakin funktiokutsua. Normaalisti se ei ole iso asia, mutta C++:ssa haluamme mahdollistaa erittäin suorituskykyisen koodin. Toisin sanoen C++:lla on ”maksa siitä vain, jos käytät sitä” -filosofia, mikä tarkoittaa, että kieli ei saa koskaan mielivaltaisesti asettaa mitään ylikuormitusta yli sen, mitä fyysinen kone pystyy suorittamaan (tietysti ohjelmoija voi valinnaisesti käyttää dynaamisen sitomisen kaltaisia tekniikoita, jotka yleensä aiheuttavat jonkin verran ylikuormitusta vastineeksi joustavuudesta tai jostain muusta ”kyvykkyydestä”, mutta suunnittelija ja ohjelmoija päättävät, haluavatko he tällaisten konstruktioiden hyödyt (ja kustannukset)).

Leave a Reply