Operatörsöverbelastning i programmering är ett av sätten att implementera polymorfism , som består i möjligheten av den samtidiga existensen i samma omfång av flera olika alternativ för att använda operatörer som har samma namn, men som skiljer sig i de typer av parametrar till vilka de är applicerad.
Termen " overload " är ett spårningspapper av det engelska ordet overloading . En sådan översättning dök upp i böcker om programmeringsspråk under första hälften av 1990-talet. I sovjetperiodens publikationer kallades liknande mekanismer omdefiniering eller omdefiniering , överlappande operationer.
Ibland finns det ett behov av att beskriva och tillämpa operationer på datatyper som skapats av programmeraren och som har samma betydelse som de som redan finns tillgängliga på språket. Ett klassiskt exempel är biblioteket för att arbeta med komplexa tal . De, liksom vanliga numeriska typer, stöder aritmetiska operationer, och det skulle vara naturligt att skapa för denna typ av operation "plus", "minus", "multiplicera", "divide", och beteckna dem med samma operationstecken som för andra numeriska typer. Förbudet mot användning av element definierade i språket tvingar fram skapandet av många funktioner med namn som ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat, och så vidare.
När operationer av samma betydelse tillämpas på operander av olika typer, tvingas de namnges olika. Oförmågan att använda funktioner med samma namn för olika typer av funktioner leder till att man måste hitta på olika namn för samma sak, vilket skapar förvirring och kan till och med leda till fel. Till exempel, i det klassiska C-språket, finns det två versioner av standardbiblioteksfunktionen för att hitta modulen för ett tal: abs() och fabs() - den första är för ett heltalsargument, den andra för ett riktigt. Denna situation, i kombination med svag C-typkontroll, kan leda till ett svårtillgängligt fel: om en programmerare skriver abs(x) i beräkningen, där x är en verklig variabel, kommer vissa kompilatorer att generera kod utan förvarning som omvandla x till ett heltal genom att kassera bråkdelarna och beräkna modulen från det resulterande heltal.
Delvis löses problemet med hjälp av objektprogrammering - när nya datatyper deklareras som klasser kan operationer på dem formaliseras som klassmetoder, inklusive klassmetoder med samma namn (eftersom metoder för olika klasser inte behöver ha olika namn), men för det första är ett sådant designsätt för värderingar av olika typer obekvämt, och för det andra löser det inte problemet med att skapa nya operatörer.
Verktyg som låter dig utöka språket, komplettera det med nya operationer och syntaktiska konstruktioner (och överbelastning av operationer är ett av sådana verktyg, tillsammans med objekt, makron, funktionaliteter, stängningar) gör det till ett metaspråk - ett verktyg för att beskriva språk fokuserade på specifika uppgifter. Med dess hjälp är det möjligt att bygga en språktillägg för varje specifik uppgift som är mest lämplig för den, vilket gör det möjligt att beskriva lösningen i den mest naturliga, begripliga och enkla formen. Till exempel, i en applikation för överbelastningsoperationer: att skapa ett bibliotek av komplexa matematiska typer (vektorer, matriser) och beskriva operationer med dem i en naturlig, "matematisk" form, skapar ett "språk för vektoroperationer", där komplexiteten av beräkningar är dolda, och det är möjligt att beskriva lösningen av problem i termer av vektor- och matrisoperationer, med fokus på problemets essens, inte på tekniken. Det var av dessa skäl som sådana medel en gång ingick i Algol-68- språket .
Operatörsöverbelastning innebär införandet av två inbördes relaterade funktioner i språket: förmågan att deklarera flera procedurer eller funktioner med samma namn i samma omfång, och förmågan att beskriva dina egna implementeringar av binära operatorer (det vill säga tecken på operationer, vanligtvis skrivet med infix notation, mellan operander). I grund och botten är deras implementering ganska enkel:
Det finns fyra typer av operatörsöverbelastning i C++:
Det är viktigt att komma ihåg att överbelastning förbättrar språket, det ändrar inte språket, så du kan inte överbelasta operatörer för inbyggda typer. Du kan inte ändra prioritet och associativitet (vänster till höger eller höger till vänster) för operatorer. Du kan inte skapa dina egna operatörer och överbelasta några av de inbyggda: :: . .* ?: sizeof typeid. Operatörer && || ,förlorar också sina unika egenskaper när de överbelastas: lathet för de två första och företräde för ett kommatecken (ordningen för uttryck mellan kommatecken är strikt definierad som vänsterassociativ, det vill säga vänster till höger). Operatören ->måste returnera antingen en pekare eller ett objekt (genom kopia eller referens).
Operatörer kan överbelastas både som fristående funktioner och som medlemsfunktioner i en klass. I det andra fallet är det vänstra argumentet för operatorn alltid *detta objekt. Operatörer = -> [] ()kan bara överbelastas som metoder (medlemsfunktioner), inte som funktioner.
Du kan göra det mycket lättare att skriva kod om du överbelasta operatörer i en viss ordning. Detta kommer inte bara att påskynda skrivningen, utan också rädda dig från att duplicera samma kod. Låt oss överväga en överbelastning med exemplet på en klass som är en geometrisk punkt i ett tvådimensionellt vektorrum:
classPoint _ { int x , y ; offentliga : Punkt ( int x , int xx ) : x ( x ), y ( xx ) {} // Standardkonstruktorn är borta. // Konstruktörargumentnamn kan vara samma som klassfältnamn. }Andra operatörer omfattas inte av några allmänna riktlinjer för överbelastning.
TypkonverteringarTypkonverteringar låter dig specificera reglerna för att konvertera vår klass till andra typer och klasser. Du kan också ange den explicita specifikationen, som endast tillåter typkonvertering om programmeraren uttryckligen har angett det (till exempel static_cast<Point3>(Point(2,3)); ). Exempel:
Point :: operator bool () const { returnera detta -> x != 0 || detta -> y != 0 ; } Tilldelnings- och avallokeringsoperatörerOperatörer new new[] delete delete[]kan vara överbelastade och kan ta valfritt antal argument. Dessutom måste operatorer new и new[]ta ett typargument som det första argumentet std::size_toch returnera ett värde av typ void *, och operatorer måste ta det delete delete[]första void *och inte returnera något ( void). Dessa operatörer kan överbelastas både för funktioner och för betongklasser.
Exempel:
void * MyClass :: operator new ( std :: size_t s , int a ) { void * p = malloc ( s * a ); if ( p == nullptr ) kasta "Inget ledigt minne!" ; returnera p ; } // ... // Call: MyClass * p = new ( 12 ) MyClass ;
Anpassade bokstaver har funnits sedan den elfte C++-standarden. Bokstavar beter sig som vanliga funktioner. De kan vara inline- eller constexpr-kvalificeringar . Det är önskvärt att det bokstavliga börjar med ett understreck, eftersom det kan finnas en konflikt med framtida standarder. Till exempel hör det bokstavliga i redan till de komplexa talen från std::complex.
Bokstaver kan bara ta en av följande typer: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Det räcker med att överbelasta bokstaven endast för typen const char * . Om ingen mer lämplig kandidat hittas kommer en operatör med den typen att anropas. Ett exempel på att konvertera miles till kilometer:
constexpr int operator "" _mi ( osignerad lång lång int i ) { return 1,6 * i ;} constexpr dubbeloperator " " _mi ( lång dubbel i ) { return 1,6 * i ;}Strängliteraler tar ett andra argument std::size_toch ett av de första: const char * , const wchar_t *, const char16_t * , const char32_t *. Strängbokstavar gäller för poster inom dubbla citattecken.
C++ har en inbyggd prefixsträng bokstavlig R som behandlar alla citattecken som vanliga tecken och inte tolkar vissa sekvenser som specialtecken. Till exempel kommer ett sådant kommando std::cout << R"(Hello!\n)"att visa Hello!\n.
Operatörsöverbelastning är nära relaterad till metodöverbelastning. En operatör är överbelastad med nyckelordet Operatör, som definierar en "operatörsmetod", som i sin tur definierar operatörens agerande med avseende på dess klass. Det finns två former av operatormetoder (operator): en för unära operatorer , den andra för binära . Nedan är den allmänna formen för varje variant av dessa metoder.
// allmän form av unär operatörsöverbelastning. public static return_type operator op ( parameter_type operand ) { // operations } // Allmän form av binär operatoröverbelastning. public static return_type operator op ( parameter_type1 operand1 , parameter_type2 operand2 ) { // operations }Här, istället för "op", ersätts en överbelastad operator, till exempel + eller /; och "returtyp" betecknar den specifika typen av värde som returneras av den specificerade operationen. Detta värde kan vara av vilken typ som helst, men det anges ofta vara av samma typ som den klass för vilken operatören överbelastas. Denna korrelation gör det lättare att använda överbelastade operatorer i uttryck. För unära operatorer betecknar operanden den operand som skickas, och för binära operatorer betecknas densamma med "operand1 och operand2". Observera att operatörsmetoder måste vara av båda typerna, offentliga och statiska. Operandtypen för unära operatörer måste vara samma som den klass för vilken operatören överbelastas. Och i binära operatorer måste minst en av operanderna vara av samma typ som dess klass. Därför tillåter inte C# att några operatorer överbelastas på objekt som ännu inte har skapats. Tilldelningen av operatorn + kan till exempel inte åsidosättas för element av typen int eller string . Du kan inte använda ref eller out-modifieraren i operatörsparametrar. [ett]
Att överbelasta procedurer och funktioner på en generell idénivå är som regel inte svårt att implementera eller förstå. Men även i den finns det några "fallgropar" som måste beaktas. Att tillåta operatörsöverbelastning skapar mycket mer problem för både språkimplementatorn och programmeraren som arbetar på det språket.
IdentifieringsproblemDet första problemet är sammanhangsberoende . Det vill säga, den första frågan som en utvecklare av en språköversättare som tillåter överbelastning av procedurer och funktioner står inför är: hur man väljer bland procedurerna med samma namn den som ska tillämpas i det här specifika fallet? Allt är bra om det finns en variant av proceduren, vars typer av formella parametrar exakt matchar typerna av de faktiska parametrarna som används i detta samtal. Men på nästan alla språk finns det en viss grad av frihet i användningen av typer, förutsatt att kompilatorn i vissa situationer automatiskt säkert konverterar (castar) datatyper. Till exempel, i aritmetiska operationer på reella och heltalsargument, konverteras ett heltal vanligtvis automatiskt till en reell typ, och resultatet är reellt. Anta att det finns två varianter av add-funktionen:
int add(int a1, int a2); float add(float a1, float a2);Hur ska kompilatorn hantera uttrycket y = add(x, i)där x är av typen float och i är av typen int? Det finns uppenbarligen ingen exakt matchning. Det finns två alternativ: antingen y=add_int((int)x,i), eller som (här betecknas den första och andra versionen av funktionen med respektive y=add_flt(x, (float)i)namn ).add_intadd_flt
Frågan uppstår: ska kompilatorn tillåta denna användning av överbelastade funktioner, och i så fall, på vilken grund kommer den att välja den specifika variant som används? Särskilt, i exemplet ovan, bör översättaren överväga typen av variabel y när han väljer? Det bör noteras att den givna situationen är den enklaste. Men mycket mer komplicerade fall är möjliga, som förvärras av det faktum att inte bara inbyggda typer kan konverteras enligt språkets regler, utan även klasser som deklareras av programmeraren, om de har släktskapsförhållanden, kan gjutas från en till en annan. Det finns två lösningar på detta problem:
Till skillnad från procedurer och funktioner har infix-operationer för programmeringsspråk två ytterligare egenskaper som avsevärt påverkar deras funktionalitet: prioritet och associativitet , vars närvaro beror på möjligheten till "kedje"-inspelning av operatörer (hur man förstår a+b*c : hur (a+b)*celler hur a+(b*c)? Uttryck a-b+c - detta (a-b)+celler a-(b+c)?).
Operationerna som är inbyggda i språket har alltid fördefinierad traditionell företräde och associativitet. Frågan uppstår: vilka prioriteringar och associativitet kommer de omdefinierade versionerna av dessa operationer att ha, eller dessutom de nya operationerna som skapats av programmeraren? Det finns andra finesser som kan kräva förtydligande. Till exempel, i C finns det två former av inkrement- och dekrementoperatorerna ++och -- , prefix och postfix, som beter sig olika. Hur ska de överbelastade versionerna av sådana operatörer bete sig?
Olika språk hanterar dessa frågor på olika sätt. Så i C++ bevaras prioriteten och associativiteten för överbelastade versioner av operatorer på samma sätt som de för fördefinierade versioner i språket, och överbelastade beskrivningar av prefix- och postfixformerna för inkrement- och dekrementoperatorerna använder olika signaturer:
prefixform | Postfix-formulär | |
---|---|---|
Fungera | T&operatör ++(T&) | T-operator ++(T &, int) |
medlemsfunktion | T&T::operator ++() | TT::operator ++(int) |
Faktum är att operationen inte har en heltalsparameter - den är fiktiv och läggs bara till för att göra skillnad i signaturerna
En fråga till: är det möjligt att tillåta operatörsöverbelastning för inbyggda och för redan deklarerade datatyper? Kan en programmerare ändra implementeringen av tilläggsoperationen för den inbyggda integraltypen? Eller för bibliotekstypen "matris"? Den första frågan besvaras i regel nekande. Att ändra beteendet för standardoperationer för inbyggda typer är en extremt specifik åtgärd, vars verkliga behov kan uppstå endast i sällsynta fall, medan de skadliga konsekvenserna av den okontrollerade användningen av en sådan funktion är svåra att ens helt förutse. Därför förbjuder språket vanligtvis antingen omdefiniering av operationer för inbyggda typer, eller implementerar en operatörsöverbelastningsmekanism på ett sådant sätt att standardoperationer helt enkelt inte kan åsidosättas med dess hjälp. När det gäller den andra frågan (omdefiniering av operatorer som redan beskrivits för befintliga typer), tillhandahålls den nödvändiga funktionaliteten helt av mekanismen för klassarv och metodöverstyrning: om du vill ändra beteendet för en befintlig klass måste du ärva den och omdefiniera de operatörer som beskrivs i den. I det här fallet kommer den gamla klassen att förbli oförändrad, den nya kommer att få nödvändig funktionalitet och inga kollisioner kommer att inträffa.
Tillkännagivande av ny verksamhetSituationen med tillkännagivandet av nya operationer är ännu mer komplicerad. Att inkludera möjligheten till en sådan förklaring på språket är inte svårt, men dess genomförande är fyllt med betydande svårigheter. Att deklarera en ny operation är i själva verket att skapa ett nytt nyckelord för programmeringsspråk, komplicerat av det faktum att operationer i texten som regel kan följa utan separatorer med andra tokens. När de dyker upp uppstår ytterligare svårigheter i organisationen av den lexikala analysatorn. Till exempel, om språket redan har operationerna "+" och det unära "-" (teckenändring), kan uttrycket a+-bkorrekt tolkas som a + (-b), men om en ny operation deklareras i programmet +-uppstår omedelbart tvetydighet, eftersom samma uttryck kan redan analyseras och hur a (+-) b. Utvecklaren och implementeraren av språket måste hantera sådana problem på något sätt. Alternativen, återigen, kan vara olika: kräv att alla nya operationer är enstaka tecken, postulera att vid eventuella avvikelser väljs den "längsta" versionen av operationen (det vill säga tills nästa uppsättning tecken läses av översättaren matchar vilken operation som helst, den fortsätter att läsas), försök att upptäcka kollisioner under översättning och generera fel i kontroversiella fall ... På ett eller annat sätt löser språk som tillåter deklarationen av nya operationer dessa problem.
Det bör inte glömmas bort att för nya verksamheter finns det också frågan om att bestämma associativitet och prioritet. Det finns inte längre en färdig lösning i form av en standardspråkoperation, och vanligtvis är det bara att ställa in dessa parametrar med språkets regler. Gör till exempel alla nya operationer vänsterassociativa och ge dem samma, fasta, prioritet, eller introducera i språket sättet att specificera båda.
När överbelastade operatorer, funktioner och procedurer används i starkt skrivna språk, där varje variabel har en fördeklarerad typ, är det upp till kompilatorn att bestämma vilken version av den överbelastade operatorn som ska användas i varje särskilt fall, oavsett hur komplext . Detta innebär att för kompilerade språk minskar inte användningen av operatörsöverbelastning prestanda på något sätt - i alla fall finns det ett väldefinierat operations- eller funktionsanrop i programmets objektkod. Situationen är annorlunda när det är möjligt att använda polymorfa variabler i språket - variabler som kan innehålla värden av olika slag vid olika tidpunkter.
Eftersom typen av värde som den överbelastade operationen kommer att tillämpas på är okänd vid tidpunkten för kodöversättning, berövas kompilatorn möjligheten att välja det önskade alternativet i förväg. I den här situationen tvingas det bädda in ett fragment i objektkoden som, omedelbart innan den här operationen utförs, kommer att bestämma typerna av värdena i argumenten och dynamiskt välja en variant som motsvarar denna uppsättning typer. Dessutom måste en sådan definition göras varje gång operationen utförs, eftersom till och med samma kod, som kallas en andra gång, mycket väl kan exekveras annorlunda ...
Användningen av operatöröverbelastning i kombination med polymorfa variabler gör det således oundvikligt att dynamiskt bestämma vilken kod som ska anropas.
Användningen av överbelastning anses inte vara en välsignelse av alla experter. Om funktions- och proceduröverbelastning i allmänhet inte finner några allvarliga invändningar (delvis för att det inte leder till några typiska "operatörs"-problem, dels för att det är mindre frestande att missbruka det), då operatöröverbelastning, som i princip, och i specifik språkimplementationer, utsätts för ganska hård kritik från många programmeringsteoretiker och praktiker.
Kritiker påpekar att problemen med identifiering, företräde och associativitet som beskrivs ovan ofta gör det onödigt svårt eller onaturligt att hantera överbelastade operatörer:
Hur mycket bekvämligheten med att använda sin egen verksamhet kan uppväga besväret med försämrad programhanterbarhet är en fråga som inte har ett tydligt svar.
Vissa kritiker talar emot överbelastningsoperationer, baserade på de allmänna principerna för mjukvaruutvecklingsteori och verklig industriell praxis.
Detta problem följer naturligt av de två föregående. Det utjämnas lätt av acceptansen av avtal och den allmänna programmeringskulturen.
Följande är en klassificering av vissa programmeringsspråk beroende på om de tillåter operatörsöverbelastning och om operatörer är begränsade till en fördefinierad uppsättning:
Många operatörer |
Ingen överbelastning |
Det finns en överbelastning |
---|---|---|
Endast fördefinierade |
Ada | |
Det är möjligt att introducera nya |
Algol 68 |