Operatøroverbelastning i programmering er en af måderne at implementere polymorfi på, som består i muligheden for den samtidige eksistens i samme omfang af flere forskellige muligheder for at bruge operatører, der har samme navn, men adskiller sig i de typer parametre, som de er til anvendt.
Udtrykket " overload " er et kalkerpapir af det engelske ord overloading . En sådan oversættelse dukkede op i bøger om programmeringssprog i første halvdel af 1990'erne. I publikationerne fra den sovjetiske periode blev lignende mekanismer kaldt redefinition eller redefinition , overlappende operationer.
Nogle gange er der behov for at beskrive og anvende operationer på datatyper skabt af programmøren, som i betydning svarer til dem, der allerede er tilgængelige på sproget. Et klassisk eksempel er biblioteket til at arbejde med komplekse tal . De understøtter ligesom almindelige numeriske typer aritmetiske operationer, og det ville være naturligt for denne type operationer at skabe "plus", "minus", "multipliker", "divide", og angiver dem med de samme operationstegn som for andre numeriske typer. Forbuddet mod brug af elementer defineret i sproget tvinger oprettelsen af mange funktioner med navne som ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat og så videre.
Når operationer af samme betydning anvendes på operander af forskellige typer, er de tvunget til at blive navngivet forskelligt. Manglende evne til at bruge funktioner med samme navn til forskellige typer funktioner fører til behovet for at opfinde forskellige navne for den samme ting, hvilket skaber forvirring og kan endda føre til fejl. For eksempel er der i det klassiske C-sprog to versioner af standardbiblioteksfunktionen til at finde modulet af et tal: abs() og fabs() - den første er for et heltalsargument, den anden for et rigtigt. Denne situation, kombineret med svag C-typekontrol, kan føre til en svær at finde fejl: hvis en programmør skriver abs(x) i beregningen, hvor x er en reel variabel, så vil nogle compilere generere kode uden varsel, der vil konverter x til et heltal ved at kassere brøkdelene og beregn modulet ud fra det resulterende heltal.
Dels løses problemet ved hjælp af objektprogrammering - når nye datatyper erklæres som klasser, kan operationer på dem formaliseres som klassemetoder, herunder klassemetoder af samme navn (da metoder af forskellige klasser ikke behøver at have forskellige navne), men for det første er en sådan design måde at operere på værdier af forskellige typer ubelejligt, og for det andet løser det ikke problemet med at skabe nye operatører.
Værktøjer, der giver dig mulighed for at udvide sproget, supplere det med nye operationer og syntaktiske konstruktioner (og overbelastning af operationer er et af sådanne værktøjer sammen med objekter, makroer, funktionaliteter, lukninger) gør det til et metasprog - et værktøj til at beskrive sprog fokuseret på specifikke opgaver. Med dens hjælp er det muligt at bygge en sprogudvidelse til hver specifik opgave, der er mest passende for den, som gør det muligt at beskrive dens løsning i den mest naturlige, forståelige og enkle form. For eksempel i en applikation til overbelastningsoperationer: oprettelse af et bibliotek af komplekse matematiske typer (vektorer, matricer) og beskrivelse af operationer med dem i en naturlig, "matematisk" form, skaber et "sprog for vektoroperationer", hvor kompleksiteten af beregninger er skjult, og det er muligt at beskrive løsningen af problemer i form af vektor- og matrixoperationer, med fokus på problemets essens, ikke på teknikken. Det var af disse grunde, at sådanne midler engang blev inkluderet i Algol-68- sproget .
Operatøroverbelastning involverer indførelsen af to indbyrdes forbundne funktioner i sproget: evnen til at erklære flere procedurer eller funktioner med samme navn i samme omfang og evnen til at beskrive dine egne implementeringer af binære operatorer (det vil sige tegn på operationer, normalt skrevet med infix-notation, mellem operander). Grundlæggende er deres implementering ret enkel:
Der er fire typer operatøroverbelastning i C++:
Det er vigtigt at huske, at overbelastning forbedrer sproget, det ændrer ikke sproget, så du kan ikke overbelaste operatører for indbyggede typer. Du kan ikke ændre forrangen og associativiteten (venstre mod højre eller højre mod venstre) af operatorer. Du kan ikke oprette dine egne operatører og overbelaste nogle af de indbyggede: :: . .* ?: sizeof typeid. Operatører && || ,mister også deres unikke egenskaber, når de overbelastes: dovenskab for de to første og forrang for et komma (rækkefølgen af udtryk mellem kommaer er strengt defineret som venstre-associativ, dvs. venstre-til-højre). Operatøren ->skal returnere enten en pointer eller et objekt (ved kopi eller reference).
Operatører kan overbelastes både som selvstændige funktioner og som medlemsfunktioner i en klasse. I det andet tilfælde er det venstre argument for operatoren altid *dette objekt. Operatører = -> [] ()kan kun overbelastes som metoder (medlemsfunktioner), ikke som funktioner.
Du kan gøre det meget nemmere at skrive kode, hvis du overbelaster operatører i en bestemt rækkefølge. Dette vil ikke kun fremskynde skrivningen, men også spare dig for at duplikere den samme kode. Lad os overveje en overbelastning ved at bruge eksemplet på en klasse, der er et geometrisk punkt i et todimensionelt vektorrum:
classPoint _ { int x , y ; offentligt : Punkt ( int x , int xx ) : x ( x ), y ( xx ) {} // Standardkonstruktøren er væk. // Konstruktorargumentnavne kan være de samme som klassefeltnavne. }Andre operatører er ikke underlagt nogen generelle retningslinjer for overbelastning.
TypekonverteringerTypekonverteringer giver dig mulighed for at specificere reglerne for konvertering af vores klasse til andre typer og klasser. Du kan også angive den eksplicitte specifier, som kun vil tillade typekonvertering, hvis programmøren eksplicit har angivet det (for eksempel static_cast<Point3>(Point(2,3)); ). Eksempel:
Punkt :: operator bool () const { returner denne -> x != 0 || dette -> y != 0 ; } Tildelings- og deallokeringsoperatørerOperatører new new[] delete delete[]kan blive overbelastet og kan tage et vilkårligt antal argumenter. Desuden skal operatører new и new[]tage et type-argument som det første argument std::size_tog returnere en værdi af type void *, og operatører skal tage det delete delete[]første void *og ikke returnere noget ( void). Disse operatører kan overbelastes både til funktioner og til betonklasser.
Eksempel:
void * MyClass :: operator new ( std :: size_t s , int a ) { void * p = malloc ( s * a ); if ( p == nullptr ) smid "Ingen ledig hukommelse!" ; returnere p ; } // ... // Kald: MyClass * p = new ( 12 ) MyClass ;
Brugerdefinerede bogstaver har eksisteret siden den ellevte C++-standard. Bogstaver opfører sig som almindelige funktioner. De kan være inline eller constexpr qualifiers . Det er ønskeligt, at det bogstavelige begynder med en understregningstegn, da der kan være en konflikt med fremtidige standarder. For eksempel hører det bogstavelige i allerede til de komplekse tal fra std::complex.
Bogstaver kan kun tage én af følgende typer: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Det er nok kun at overbelaste det bogstavelige for typen const char * . Hvis der ikke findes en mere egnet kandidat, vil en operatør med den type blive tilkaldt. Et eksempel på konvertering af miles til kilometer:
constexpr int operator "" _mi ( usigned long long int i ) { returner 1,6 * i ;} constexpr dobbeltoperator " " _mi ( lang dobbelt i ) { returner 1,6 * i ;}Streng bogstaver tager et andet argument std::size_tog et af de første: const char * , const wchar_t *, const char16_t * , const char32_t *. Streng bogstaver gælder for indgange inden for dobbelte anførselstegn.
C++ har en indbygget præfiksstreng literal R , der behandler alle citerede tegn som almindelige tegn og ikke fortolker visse sekvenser som specialtegn. For eksempel vil en sådan kommando std::cout << R"(Hello!\n)"vise Hello!\n.
Operatøroverbelastning er tæt forbundet med metodeoverbelastning. En operatør er overbelastet med nøgleordet Operator, som definerer en "operatørmetode", som igen definerer operatørens handling i forhold til dens klasse. Der er to former for operatormetoder (operator): en for unære operatorer , den anden for binære . Nedenfor er den generelle formular for hver variation af disse metoder.
// generel form for unær operatøroverbelastning. public static return_type operator op ( parameter_type operand ) { // operations } // Generel form for binær operator overbelastning. public static return_type operator op ( parameter_type1 operand1 , parameter_type2 operand2 ) { // operations }Her erstattes en overbelastet operator i stedet for "op", for eksempel + eller /; og "return_type" angiver den specifikke type værdi, der returneres af den specificerede operation. Denne værdi kan være af enhver type, men den er ofte angivet til at være af samme type som den klasse, som operatøren bliver overbelastet for. Denne korrelation gør det lettere at bruge overbelastede operatorer i udtryk. For unære operatorer angiver operanden operanden, der sendes, og for binære operatorer er det samme betegnet med "operand1 og operand2". Bemærk, at operatørmetoder skal være af begge typer, offentlige og statiske. Operandtypen af unære operatører skal være den samme som den klasse, som operatøren bliver overbelastet for. Og i binære operatorer skal mindst én af operanderne være af samme type som dens klasse. Derfor tillader C# ikke, at nogen operatører overbelastes på objekter, der endnu ikke er oprettet. For eksempel kan tildelingen af operatoren + ikke tilsidesættes for elementer af typen int eller streng . Du kan ikke bruge ref eller ud-modifikatoren i operatørparametre. [en]
Overbelastning af procedurer og funktioner på niveau med en generel idé er som regel ikke vanskeligt hverken at implementere eller at forstå. Men selv i den er der nogle "faldgruber", der skal overvejes. At tillade operatøroverbelastning skaber mange flere problemer for både sprogimplementatoren og programmøren, der arbejder på det sprog.
IdentifikationsproblemDet første problem er kontekstafhængighed . Det vil sige, det første spørgsmål, som en udvikler af en sprogoversætter, der tillader overbelastning af procedurer og funktioner står over for, er: hvordan vælger man blandt procedurerne af samme navn den, der skal anvendes i dette særlige tilfælde? Alt er fint, hvis der er en variant af proceduren, hvis typer af formelle parametre nøjagtigt matcher typerne af de faktiske parametre, der bruges i dette opkald. På næsten alle sprog er der dog en vis grad af frihed i brugen af typer, forudsat at compileren i visse situationer automatisk konverterer (caster) datatyper sikkert. For eksempel, i aritmetiske operationer på reelle og heltalsargumenter konverteres et heltal normalt automatisk til en reel type, og resultatet er reelt. Antag, at der er to varianter af tilføjelsesfunktionen:
int add(int a1, int a2); float add(float a1, float a2);Hvordan skal compileren håndtere udtrykket y = add(x, i), hvor x er af typen float og i er af typen int? Der er åbenbart ikke noget nøjagtigt match. Der er to muligheder: enten y=add_int((int)x,i), eller som (her er den første og anden version af funktionen angivet med y=add_flt(x, (float)i)henholdsvis navnene add_intog ).add_flt
Spørgsmålet opstår: skal compileren tillade denne brug af overbelastede funktioner, og i så fald, på hvilket grundlag vil den vælge den anvendte variant? Skal oversætteren især i eksemplet ovenfor overveje typen af variablen y, når han vælger? Det skal bemærkes, at den givne situation er den enkleste. Men meget mere komplicerede sager er mulige, som forværres af, at ikke kun indbyggede typer kan konverteres efter sprogets regler, men også klasser deklareret af programmøren, hvis de har slægtskabsforhold, kan castes fra den ene til den anden. Der er to løsninger på dette problem:
I modsætning til procedurer og funktioner har infix-operationer af programmeringssprog to yderligere egenskaber, der væsentligt påvirker deres funktionalitet: prioritet og associativitet , hvis tilstedeværelse skyldes muligheden for "kæde"-optagelse af operatører (hvordan man forstår a+b*c : hvordan (a+b)*celler hvordan a+(b*c)? Udtryk a-b+c - dette (a-b)+celler a-(b+c)?).
Operationerne indbygget i sproget har altid foruddefineret traditionel forrang og associativitet. Spørgsmålet opstår: hvilke prioriteter og associativitet vil de omdefinerede versioner af disse operationer have, eller i øvrigt de nye operationer skabt af programmøren? Der er andre finesser, der kan kræve afklaring. For eksempel er der i C to former for inkrement- og dekrementoperatorerne ++og -- , præfiks og postfiks, som opfører sig forskelligt. Hvordan skal de overbelastede versioner af sådanne operatører opføre sig?
Forskellige sprog håndterer disse problemer på forskellige måder. Så i C++ er forrangen og associativiteten af overbelastede versioner af operatorer bevaret den samme som dem for foruddefinerede på sproget, og overbelastningsbeskrivelser af præfiks- og postfix-formerne for inkrement- og dekrementoperatorerne bruger forskellige signaturer:
præfiksform | Postfix formular | |
---|---|---|
Fungere | T&operatør ++(T&) | T-operator ++(T &, int) |
medlemsfunktion | T&T::operator ++() | TT::operator ++(int) |
Faktisk har operationen ikke en heltalsparameter - den er fiktiv og tilføjes kun for at gøre en forskel i signaturerne
Et spørgsmål mere: er det muligt at tillade operatøroverbelastning for indbyggede og allerede erklærede datatyper? Kan en programmør ændre implementeringen af additionsoperationen for den indbyggede heltalstype? Eller for bibliotekstypen "matrix"? Som regel besvares det første spørgsmål benægtende. Ændring af adfærden for standardoperationer for indbyggede typer er en ekstremt specifik handling, hvis reelle behov kun kan opstå i sjældne tilfælde, mens de skadelige konsekvenser af den ukontrollerede brug af en sådan funktion er svære endda helt at forudsige. Derfor forbyder sproget normalt enten at omdefinere operationer for indbyggede typer eller implementerer en operatøroverbelastningsmekanisme på en sådan måde, at standardoperationer simpelthen ikke kan tilsidesættes med dens hjælp. Hvad angår det andet spørgsmål (omdefinering af operatører, der allerede er beskrevet for eksisterende typer), er den nødvendige funktionalitet fuldt ud tilvejebragt af mekanismen for klassearv og metodetilsidesættelse: hvis du vil ændre adfærden for en eksisterende klasse, skal du arve den og omdefinere operatørerne beskrevet i den. I dette tilfælde forbliver den gamle klasse uændret, den nye vil modtage den nødvendige funktionalitet, og der vil ikke forekomme kollisioner.
Annoncering af nye operationerSituationen med annonceringen af nye operationer er endnu mere kompliceret. Det er ikke svært at inkludere muligheden for en sådan erklæring på sproget, men dens gennemførelse er fyldt med betydelige vanskeligheder. At erklære en ny operation er i virkeligheden at skabe et nyt programmeringssprog-nøgleord, kompliceret af det faktum, at operationer i teksten som regel kan følge uden separatorer med andre tokens. Når de dukker op, opstår der yderligere vanskeligheder i organiseringen af den leksikalske analysator. For eksempel, hvis sproget allerede har operationerne "+" og det unære "-" (tegnskifte), så kan udtrykket a+-btolkes nøjagtigt som a + (-b), men hvis en ny operation er erklæret i programmet +-, opstår der straks tvetydighed, fordi samme udtryk kan allerede parses og hvordan a (+-) b. Udvikleren og implementeren af sproget skal håndtere sådanne problemer på en eller anden måde. Mulighederne kan igen være forskellige: kræve, at alle nye operationer er et-tegn, postuler, at i tilfælde af uoverensstemmelser, vælges den "længste" version af operationen (det vil sige indtil det næste sæt tegn, der læses af oversætter matcher enhver operation, den fortsætter med at blive læst), prøv at opdage kollisioner under oversættelse og generere fejl i kontroversielle tilfælde ... På en eller anden måde løser sprog, der tillader erklæringen af nye operationer, disse problemer.
Det bør ikke glemmes, at for nye operationer er der også spørgsmålet om at bestemme associativitet og prioritet. Der findes ikke længere en færdig løsning i form af en standardsprogoperation, og normalt skal man bare indstille disse parametre med sprogets regler. Gør for eksempel alle nye operationer venstreassociative og giv dem den samme, faste prioritet, eller indfør i sproget midlerne til at specificere begge dele.
Når overbelastede operatører, funktioner og procedurer bruges i stærkt indtastede sprog, hvor hver variabel har en forud-erklæret type, er det op til compileren at beslutte, hvilken version af den overbelastede operatør, der skal bruges i hvert enkelt tilfælde, uanset hvor kompleks . Det betyder, at for kompilerede sprog reducerer brugen af operatøroverbelastning ikke ydeevnen på nogen måde - under alle omstændigheder er der en veldefineret operation eller funktionskald i programmets objektkode. Situationen er anderledes, når det er muligt at bruge polymorfe variable i sproget - variable, der kan indeholde værdier af forskellige typer på forskellige tidspunkter.
Da typen af den værdi, som den overbelastede operation vil blive anvendt på, er ukendt på tidspunktet for kodeoversættelsen, er compileren frataget muligheden for at vælge den ønskede mulighed på forhånd. I denne situation er det tvunget til at indlejre et fragment i objektkoden, der umiddelbart før udførelse af denne operation vil bestemme typerne af værdierne i argumenterne og dynamisk vælge en variant svarende til dette sæt typer. Desuden skal en sådan definition laves hver gang operationen udføres, fordi selv den samme kode, der kaldes en anden gang, meget vel kan udføres anderledes ...
Brugen af operatøroverbelastning i kombination med polymorfe variable gør det således uundgåeligt dynamisk at bestemme, hvilken kode der skal kaldes.
Brugen af overbelastning betragtes ikke som en velsignelse af alle eksperter. Hvis funktions- og procedureoverbelastning generelt ikke finder nogen alvorlige indvendinger (delvis fordi det ikke fører til nogle typiske "operatør" problemer, dels fordi det er mindre fristende at misbruge det), så operatør overbelastning, som i princippet, og i specifik sprogimplementeringer, er udsat for ganske alvorlig kritik fra mange programmeringsteoretikere og praktikere.
Kritikere påpeger, at problemerne med identifikation, forrang og associativitet skitseret ovenfor ofte gør håndteringen af overbelastede operatører enten unødvendigt vanskelig eller unaturlig:
Hvor meget bekvemmeligheden ved at bruge dine egne operationer kan opveje ulejligheden ved forringet programadministration er et spørgsmål, der ikke har et klart svar.
Nogle kritikere taler imod overbelastningsoperationer baseret på de generelle principper for softwareudviklingsteori og reel industriel praksis.
Dette problem følger naturligvis af de to foregående. Det er let udjævnet af accepten af aftaler og den generelle programmeringskultur.
Det følgende er en klassificering af nogle programmeringssprog, alt efter om de tillader operatøroverbelastning, og om operatører er begrænset til et foruddefineret sæt:
Mange operatører |
Ingen overbelastning |
Der er en overbelastning |
---|---|---|
Kun foruddefineret |
Ada | |
Det er muligt at introducere nyt |
Algol 68 |