Operaattoreiden ylikuormitus ohjelmoinnissa on yksi tavoista toteuttaa polymorfismi , joka koostuu mahdollisuudesta olla samanaikaisesti samassa laajuudessa useita eri vaihtoehtoja käyttää operaattoreita, joilla on sama nimi, mutta jotka eroavat parametrien tyypeistä, joihin ne on tarkoitettu. sovelletaan.
Termi " overload " on kuultopaperi englanninkielisestä sanasta overloading . Tällainen käännös ilmestyi ohjelmointikieliä koskevissa kirjoissa 1990-luvun ensimmäisellä puoliskolla. Neuvostoajan julkaisuissa vastaavia mekanismeja kutsuttiin uudelleenmäärittelyksi tai uudelleenmäärittelyksi , päällekkäisiksi toimiksi.
Joskus on tarve kuvata ja soveltaa ohjelmoijan luomiin tietotyyppeihin operaatioita, jotka vastaavat merkitykseltään kielellä jo saatavilla olevia. Klassinen esimerkki on kirjasto kompleksilukujen käsittelyä varten . Ne, kuten tavalliset numeeriset tyypit, tukevat aritmeettisia operaatioita, ja tälle operaatiolle olisi luonnollista luoda "plus", "miinus", "kerto", "jakaa" merkitsemällä ne samoilla operaatiomerkeillä kuin muille numeerisille operaatioille. tyypit. Kielellä määriteltyjen elementtien käyttökielto pakottaa luomaan monia funktioita nimillä, kuten ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat ja niin edelleen.
Kun eri tyyppisiin operandeihin sovelletaan samaa merkitystä omaavia operaatioita, ne pakotetaan nimeämään eri tavalla. Kyvyttömyys käyttää samannimisiä funktioita erityyppisille funktioille johtaa siihen, että samalle asialle on keksittävä eri nimiä, mikä aiheuttaa sekaannusta ja voi jopa johtaa virheisiin. Esimerkiksi klassisessa C-kielessä on kaksi versiota vakiokirjastofunktiosta luvun moduulin löytämiseksi: abs() ja fabs() - ensimmäinen on kokonaislukuargumenttia varten, toinen todellista argumenttia varten. Tämä tilanne yhdistettynä heikon C-tyypin tarkistukseen voi johtaa vaikeasti löydettäviin virheisiin: jos ohjelmoija kirjoittaa abs(x):n laskelmaan, missä x on todellinen muuttuja, jotkin kääntäjät generoivat koodia ilman varoitusta, joka muuntaa x kokonaisluvuksi hylkäämällä murto-osat ja laskea moduuli tuloksena olevasta kokonaisluvusta.
Osittain ongelma ratkeaa olioohjelmoinnin avulla - kun uudet tietotyypit määritellään luokiksi, niille tehtävät toiminnot voidaan formalisoida luokkametodeina, mukaan lukien samannimiset luokkamenetelmät (koska eri luokkien menetelmillä ei tarvitse olla eri nimet), mutta ensinnäkin tällainen suunnittelutapa erityyppisille arvoille on hankalaa, ja toiseksi se ei ratkaise uusien operaattoreiden luomisen ongelmaa.
Työkalut, joilla voit laajentaa kieltä, täydentää sitä uusilla operaatioilla ja syntaktisilla rakenteilla (ja operaatioiden ylikuormitus on yksi tällaisista työkaluista objektien, makrojen, funktionaalisten toimintojen, sulkemisten ohella) muuttavat kielen metakieleksi - työkaluksi kielten kuvaamiseen keskittynyt tiettyihin tehtäviin. Sen avulla on mahdollista rakentaa kullekin tietylle tehtävälle sille sopivin kielilaajennus, jonka avulla sen ratkaisua voidaan kuvata luonnollisimmassa, ymmärrettävämmässä ja yksinkertaisimmassa muodossa. Esimerkiksi sovelluksessa operaatioiden ylikuormitusta varten: monimutkaisten matemaattisten tyyppien (vektorit, matriisit) kirjaston luominen ja operaatioiden kuvaaminen niillä luonnollisessa, "matemaattisessa" muodossa, luo "vektorioperaatioiden kielen", jossa monimutkaisuus laskelmat on piilotettu, ja ongelmien ratkaisua on mahdollista kuvata vektori- ja matriisioperaatioilla keskittyen ongelman olemukseen, ei tekniikkaan. Näistä syistä sellaiset keinot sisällytettiin kerran Algol-68 -kieleen .
Operaattoreiden ylikuormitukseen sisältyy kahden toisiinsa liittyvän ominaisuuden tuominen kieleen: kyky ilmoittaa useita samalla nimellä olevia proseduureja tai funktioita samassa laajuudessa ja kyky kuvata omia binäärioperaattoreiden toteutuksia (eli operaatioiden merkkejä, yleensä kirjoitettu infix-merkinnällä, operandien väliin). Periaatteessa niiden toteutus on melko yksinkertaista:
C++:ssa on neljä erilaista operaattorin ylikuormitusta:
On tärkeää muistaa, että ylikuormitus parantaa kieltä, se ei muuta kieltä, joten sisäänrakennettujen tyyppien operaattoreita ei voi ylikuormittaa. Et voi muuttaa operaattoreiden ensisijaisuutta ja assosiatiivisuutta (vasemmalta oikealle tai oikealta vasemmalle). Et voi luoda omia operaattoreita ja ylikuormittaa joitain sisäänrakennetuista: :: . .* ?: sizeof typeid. Lisäksi operaattorit && || ,menettävät ainutlaatuiset ominaisuutensa ylikuormitettuina: laiskuus kahdelle ensimmäiselle ja pilkun ensisijaisuus (pilkkujen välinen ilmaisujärjestys on tiukasti määritelty vasen assosiatiiviseksi eli vasemmalta oikealle). Operaattorin ->on palautettava joko osoitin tai objekti (kopiona tai viittauksella).
Operaattoreita voidaan ylikuormittaa sekä itsenäisinä funktioina että luokan jäsenfunktioina. Toisessa tapauksessa operaattorin vasen argumentti on aina *this-objekti. Operaattoreita = -> [] ()voidaan ylikuormittaa vain menetelminä (jäsenfunktioina), ei funktioina.
Voit tehdä koodin kirjoittamisesta paljon helpompaa, jos ylikuormitat operaattorit tietyssä järjestyksessä. Tämä ei vain nopeuttaa kirjoittamista, vaan myös säästää sinua saman koodin kopioimisesta. Tarkastellaan ylikuormitusta käyttämällä esimerkkiä luokasta, joka on geometrinen piste kaksiulotteisessa vektoriavaruudessa:
luokkapiste _ { int x , y ; julkinen : Piste ( int x , int xx ) : x ( x ), y ( xx ) {} // Oletuskonstruktori on poissa. // Rakentajan argumenttien nimet voivat olla samat kuin luokkakenttien nimet. }Muut käyttäjät eivät ole yleisten ylikuormitusohjeiden alaisia.
Kirjoita muunnoksetTyyppimuunnoksilla voit määrittää säännöt luokkamme muuntamiseksi muihin tyyppeihin ja luokkiin. Voit myös määrittää eksplisiittisen määritteen, joka sallii tyypin muuntamisen vain, jos ohjelmoija on sen erikseen määrittänyt (esim . static_cast<Piste3>(Piste(2,3)); ). Esimerkki:
Piste :: operaattori bool () const { palauta tämä -> x != 0 || tämä -> y != 0 ; } Jako- ja jakeluoperaattoritOperaattorit new new[] delete delete[]voivat olla ylikuormitettuja ja ne voivat ottaa useita argumentteja. Lisäksi operaattoreiden new и new[]on otettava tyyppiargumentti ensimmäisenä argumenttina std::size_tja palautettava arvo tyyppi void *, ja operaattoreiden on otettava delete delete[]ensimmäinen void *eikä palautettava mitään ( void). Nämä operaattorit voivat olla ylikuormitettuja sekä toimintojen että betoniluokkien osalta.
Esimerkki:
void * MyClass :: operaattori uusi ( std :: size_t s , int a ) { void * p = malloc ( s * a ); if ( p == nullptr ) heittää "Ei vapaata muistia!" ; paluu p ; } // ... // Soita: MyClass * p = uusi ( 12 ) MyClass ;
Mukautetut literaalit ovat olleet käytössä yhdestoista C++-standardista lähtien. Literaalit toimivat kuten tavalliset funktiot. Ne voivat olla tekstin sisäisiä tai constexpr-määritteitä . On toivottavaa, että kirjaimellinen alkaa alaviivalla, koska se voi olla ristiriidassa tulevien standardien kanssa. Esimerkiksi literaali i kuuluu jo kompleksilukuihin alkaen std::complex.
Literaalit voivat olla vain yhden seuraavista tyypeistä: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Literaalin ylikuormittaminen riittää vain tyypille const char * . Jos sopivaa ehdokasta ei löydy, kutsutaan kyseisen tyyppistä operaattoria. Esimerkki mailien muuntamisesta kilometreiksi:
constexpr int operaattori "" _mi ( unsigned long long int i ) { paluu 1.6 * i ;} constexpr kaksoisoperaattori " " _mi ( pitkä kaksoisoperaattori i ) { paluu 1.6 * i ;}Merkkijonoliteraaleilla on toinen argumentti std::size_tja yksi ensimmäisistä: const char * , const wchar_t *, const char16_t * , const char32_t *. Merkkijonoliteraalit koskevat lainausmerkeissä olevia merkintöjä.
C++:ssa on sisäänrakennettu etuliitemerkkijono literaali R , joka käsittelee kaikkia lainausmerkkejä tavallisina merkeinä eikä tulkitse tiettyjä sarjoja erikoismerkeiksi. Esimerkiksi tällainen komento std::cout << R"(Hello!\n)"näyttää Hello!\n.
Operaattorin ylikuormitus liittyy läheisesti menetelmän ylikuormitukseen. Operaattori on ylikuormitettu avainsanalla Operator, joka määrittelee "operaattorimenetelmän", joka puolestaan määrittelee operaattorin toiminnan suhteessa sen luokkaan. Operaattorimenetelmiä (operaattori) on kahta muotoa : yksi unaarioperaattoreille ja toinen binäärioperaattoreille . Alla on näiden menetelmien kunkin muunnelman yleinen lomake.
// yksipuolisen operaattorin ylikuormituksen yleinen muoto. julkinen staattinen paluutyypin operaattori op ( parametrityyppi operandi ) { // toiminnot } // Binäärioperaattorin ylikuormituksen yleinen muoto. julkinen staattinen paluutyypin operaattori op ( parametrin_tyyppi1 operandi1 , parametrin_tyyppi2 operandi2 ) { // toiminnot }Tässä "op":n sijaan korvataan ylikuormitettu operaattori, esimerkiksi + tai /; ja "paluutyyppi" tarkoittaa määritetyn toiminnon palauttaman arvon tiettyä tyyppiä. Tämä arvo voi olla mitä tahansa tyyppiä, mutta se määritetään usein olevan samaa tyyppiä kuin luokka, jonka operaattoria ylikuormitetaan. Tämä korrelaatio helpottaa ylikuormitettujen operaattoreiden käyttöä lausekkeissa. Unaarisilla operaattoreilla operandi tarkoittaa välitettävää operandia, ja binäärioperaattoreille samaa merkitään "operandi1 ja operandi2". Huomaa, että operaattorimenetelmien on oltava sekä julkisia että staattisia. Unaarioperaattorien operandityypin on oltava sama kuin luokka, jonka operaattoria ylikuormitetaan. Ja binäärioperaattoreissa ainakin yhden operandin on oltava samaa tyyppiä kuin sen luokka. Siksi C# ei salli operaattoreiden ylikuormittamista objekteihin, joita ei ole vielä luotu. Esimerkiksi +-operaattorin määritystä ei voi ohittaa elementeille, joiden tyyppi on int tai string . Et voi käyttää ref- tai out-muuttujaa operaattoriparametreissa. [yksi]
Proseduurien ja toimintojen ylikuormittaminen yleisidean tasolla ei pääsääntöisesti ole vaikea toteuttaa tai ymmärtää. Kuitenkin myös siinä on joitain "sudenkuoppia", jotka on otettava huomioon. Operaattorin ylikuormituksen salliminen aiheuttaa paljon enemmän ongelmia sekä kielen toteuttajalle että tällä kielellä työskentelevälle ohjelmoijalle.
TunnistusongelmaEnsimmäinen ongelma on kontekstiriippuvuus . Eli ensimmäinen kysymys, jonka prosessien ja toimintojen ylikuormituksen sallivan kielenkääntäjän kehittäjä kohtaa, on: kuinka valita samannimistä menettelyistä se, jota tässä tapauksessa tulisi soveltaa? Kaikki on kunnossa, jos proseduurista on variantti, jonka muodollisten parametrien tyypit vastaavat täsmälleen tässä kutsussa käytettyjen todellisten parametrien tyyppejä. Kuitenkin lähes kaikilla kielillä on jonkin verran vapautta tyyppien käytössä olettaen, että kääntäjä tietyissä tilanteissa muuntaa (lähettää) tietotyypit automaattisesti turvallisesti. Esimerkiksi reaali- ja kokonaislukuargumenttien aritmeettisissa operaatioissa kokonaisluku muunnetaan yleensä reaalityypiksi automaattisesti, ja tulos on todellinen. Oletetaan, että lisäysfunktiosta on kaksi muunnelmaa:
int add(int a1, int a2); float add(kelluke a1, float a2);Miten kääntäjän tulee käsitellä lauseketta y = add(x, i), jossa x on tyyppiä float ja i on tyyppiä int? Ilmeisesti ei ole tarkkaa vastaavuutta. Vaihtoehtoja on kaksi: joko y=add_int((int)x,i), tai as (tässä funktion ensimmäinen ja toinen versio on merkitty y=add_flt(x, (float)i)nimillä add_intja vastaavasti).add_flt
Herää kysymys: pitäisikö kääntäjän sallia tämä ylikuormitettujen funktioiden käyttö, ja jos, niin millä perusteella se valitsee käytetyn muunnelman? Pitäisikö kääntäjän erityisesti yllä olevassa esimerkissä ottaa huomioon muuttujan y tyyppi valinnassa? On huomattava, että annettu tilanne on yksinkertaisin. Mutta paljon monimutkaisemmat tapaukset ovat mahdollisia, joita pahentaa se, että ei vain sisäänrakennettuja tyyppejä voidaan muuntaa kielen sääntöjen mukaan, vaan myös ohjelmoijan ilmoittamia luokkia, jos niillä on sukulaissuhteita, voidaan lähettää yksi toiselle. Tähän ongelmaan on kaksi ratkaisua:
Toisin kuin menettelyt ja toiminnot, ohjelmointikielten infix-toiminnoilla on kaksi lisäominaisuutta, jotka vaikuttavat merkittävästi niiden toimivuuteen: prioriteetti ja assosiatiivisuus , joiden olemassaolo johtuu mahdollisuudesta "ketjuttaa" operaattoreita (miten ymmärtää a+b*c : miten (a+b)*ctai miten a+(b*c)? Ilmaus a-b+c - tämä (a-b)+cvai a-(b+c)?) .
Kieleen sisäänrakennetuilla operaatioilla on aina ennalta määritelty perinteinen etusija ja assosiaatio. Herää kysymys: mitkä prioriteetit ja assosiatiivisuus ovat näiden operaatioiden uudelleenmääritellyillä versioilla tai lisäksi ohjelmoijan luomilla uusilla operaatioilla? On muitakin yksityiskohtia, jotka saattavat vaatia selvennystä. Esimerkiksi C:ssä on kaksi muotoa lisäys- ja vähennysoperaattoreita ++sekä -- , etuliite ja jälkiliite, jotka toimivat eri tavalla. Miten tällaisten operaattoreiden ylikuormitettujen versioiden pitäisi käyttäytyä?
Eri kielet käsittelevät näitä asioita eri tavoin. Joten C++:ssa operaattorien ylikuormitettujen versioiden ensisijaisuus ja assosiatiivisuus säilyvät samoina kuin kielessä ennalta määritettyjen versioiden, ja lisäys- ja vähennysoperaattoreiden etu- ja jälkiliitemuotojen ylikuormituskuvaukset käyttävät erilaisia allekirjoituksia:
etuliitemuoto | Postfix lomake | |
---|---|---|
Toiminto | T&operaattori ++(T&) | T-operaattori ++(T &, int) |
jäsentoiminto | T&T::operaattori ++() | TT::operaattori ++(int) |
Itse asiassa operaatiolla ei ole kokonaislukuparametria - se on kuvitteellinen ja lisätään vain allekirjoitusten muuttamisen vuoksi
Vielä yksi kysymys: onko mahdollista sallia operaattorin ylikuormitus sisäänrakennetuille ja jo ilmoitettuille tietotyypeille? Voiko ohjelmoija muuttaa sisäänrakennetun integraalityypin lisäysoperaation toteutusta? Tai kirjastotyypille "matriisi"? Yleensä ensimmäiseen kysymykseen vastataan kieltävästi. Sisäänrakennettujen tyyppien standarditoimintojen käyttäytymisen muuttaminen on erittäin spesifinen toimenpide, jonka todellinen tarve voi ilmaantua vain harvoissa tapauksissa, kun taas tällaisen ominaisuuden hallitsemattoman käytön haitallisia seurauksia on vaikea edes täysin ennustaa. Siksi kieli yleensä joko kieltää toimintojen uudelleenmäärittelyn sisäänrakennetuille tyypeille tai toteuttaa operaattorin ylikuormitusmekanismin siten, että sen avulla ei yksinkertaisesti voida ohittaa vakiotoimintoja. Mitä tulee toiseen kysymykseen (olemassa oleville tyypeille jo kuvattujen operaattoreiden uudelleenmäärittely), tarvittavat toiminnot tarjoavat täysin luokan periytymismekanismi ja menetelmän ohitus: jos haluat muuttaa olemassa olevan luokan käyttäytymistä, sinun on perittävä se ja määritettävä uudelleen. siinä kuvatut operaattorit. Tässä tapauksessa vanha luokka pysyy ennallaan, uusi saa tarvittavat toiminnot, eikä törmäyksiä tapahdu.
Ilmoitus uusista toiminnoistaUusien toimintojen julkistamisen tilanne on vielä monimutkaisempi. Tällaisen julistuksen mahdollisuuden sisällyttäminen kieleen ei ole vaikeaa, mutta sen täytäntöönpano on täynnä merkittäviä vaikeuksia. Uuden toiminnon ilmoittaminen on itse asiassa uuden ohjelmointikielen avainsanan luomista, jota vaikeuttaa se, että tekstissä olevat toiminnot voivat pääsääntöisesti seurata ilman erottimia muiden merkkien kanssa. Kun ne ilmestyvät, leksikaalisen analysaattorin järjestämisessä syntyy lisävaikeuksia. Esimerkiksi, jos kielessä on jo operaatiot "+" ja unaari "-" (merkkimuutos), niin lauseke a+-bvoidaan tulkita tarkasti muodossa a + (-b), mutta jos ohjelmassa ilmoitetaan uusi operaatio +-, syntyy heti epäselvyyttä, koska sama lauseke voidaan jo jäsentää ja miten a (+-) b. Kielen kehittäjän ja toteuttajan tulee käsitellä tällaisia ongelmia jollain tavalla. Vaihtoehdot voivat taas olla erilaisia: vaatia, että kaikki uudet toiminnot ovat yksimerkkisiä, oletetaan, että mahdollisten eroavaisuuksien sattuessa valitaan operaation "pisin" versio (eli kunnes seuraava merkkisarja, jonka lukee kääntäjä vastaa mitä tahansa toimintoa, sen lukeminen jatkuu), yritä havaita törmäykset käännöksen aikana ja tuottaa virheitä kiistanalaisissa tapauksissa ... Tavalla tai toisella kielet, jotka mahdollistavat uusien toimintojen ilmoittamisen, ratkaisevat nämä ongelmat.
Ei pidä unohtaa, että uusien toimintojen yhteydessä on myös kysymys assosiatiivisuuden ja prioriteetin määrittämisestä. Vakiokielioperaation muodossa ei ole enää valmiita ratkaisuja, ja yleensä sinun on vain asetettava nämä parametrit kielen säännöillä. Tee esimerkiksi kaikista uusista operaatioista vasen-assosiatiivisia ja anna niille sama, kiinteä, prioriteetti tai ota käyttöön kieleen keino molempien määrittämiseen.
Kun ylikuormitettuja operaattoreita, toimintoja ja proseduureja käytetään vahvasti kirjoitetuissa kielissä, joissa jokaisella muuttujalla on ennalta määritetty tyyppi, kääntäjä päättää, mitä ylikuormitetun operaattorin versiota käyttää kussakin tapauksessa, olipa se kuinka monimutkainen tahansa. . Tämä tarkoittaa, että käännetyillä kielillä operaattorin ylikuormituksen käyttö ei heikennä suorituskykyä millään tavalla - joka tapauksessa ohjelman objektikoodissa on hyvin määritelty operaatio tai funktiokutsu. Tilanne on erilainen, kun kielessä on mahdollista käyttää polymorfisia muuttujia - muuttujia, jotka voivat sisältää eri tyyppisiä arvoja eri aikoina.
Koska arvon tyyppi, johon ylikuormitettu operaatio sovelletaan, ei ole tiedossa koodin käännöshetkellä, kääntäjä ei voi valita haluttua vaihtoehtoa etukäteen. Tässä tilanteessa objektikoodiin on pakko upottaa fragmentti, joka välittömästi ennen tämän toiminnon suorittamista määrittää argumenttien arvojen tyypit ja valitsee dynaamisesti tätä tyyppijoukkoa vastaavan muunnelman. Lisäksi tällainen määritelmä on tehtävä joka kerta, kun operaatio suoritetaan, koska jopa sama koodi, joka kutsutaan toisen kerran, voidaan suorittaa eri tavalla ...
Siten operaattorin ylikuormituksen käyttö yhdessä polymorfisten muuttujien kanssa tekee väistämättömäksi dynaamisesti määrittää, mitä koodia kutsutaan.
Kaikki asiantuntijat eivät pidä ylikuormituksen käyttöä siunauksena. Jos toimintojen ja toimintojen ylikuormitus ei yleensä löydä vakavia vastalauseita (osittain siksi, että se ei johda joihinkin tyypillisiin "operaattorin" ongelmiin, osittain siksi, että sen väärinkäyttö on vähemmän houkuttelevaa), niin operaattorin ylikuormitus, kuten periaatteessa, ja erityisesti kielitoteutuksia, on useiden ohjelmointiteoreetikojen ja -harjoittajien kohtaama melko ankara kritiikki.
Kriitikot huomauttavat, että yllä kuvatut identifiointi-, etusija- ja assosiatiivisuusongelmat tekevät usein ylikuormitettujen operaattoreiden käsittelystä tarpeettoman vaikeaa tai luonnotonta:
Se, kuinka paljon omien toimintojen käyttömukavuus voi painaa ohjelman hallittavuuden heikkenemisestä aiheutuvan haitan, on kysymys, johon ei ole yksiselitteistä vastausta.
Jotkut kriitikot vastustavat ylikuormitusta ohjelmistokehityksen teorian yleisten periaatteiden ja todellisen teollisen käytännön perusteella.
Tämä ongelma on luonnollisesti seurausta kahdesta edellisestä. Se on helppo tasoittaa sopimusten hyväksymisellä ja yleisellä ohjelmointikulttuurilla.
Seuraavassa on joidenkin ohjelmointikielten luokitus sen mukaan, sallivatko ne operaattorin ylikuormituksen ja ovatko operaattorit rajoitettu ennalta määritettyyn joukkoon:
Monet Operaattorit |
Ei ylikuormitusta |
On ylikuormitusta |
---|---|---|
Vain ennalta määritetty |
Ada | |
On mahdollista ottaa käyttöön uusia |
Algol 68 |