Supraîncărcarea operatorului

Versiunea actuală a paginii nu a fost încă examinată de colaboratori experimentați și poate diferi semnificativ de versiunea revizuită la 9 iulie 2018; verificările necesită 25 de modificări .

Supraîncărcarea operatorului în programare  este una dintre modalitățile de implementare a polimorfismului , care constă în posibilitatea existenței simultane în același domeniu a mai multor opțiuni diferite de utilizare a operatorilor care au același nume, dar diferă prin tipurile de parametri la care sunt. aplicat.

Terminologie

Termenul " supraîncărcare " este o hârtie de calc a cuvântului englezesc supraîncărcare . O astfel de traducere a apărut în cărțile despre limbaje de programare în prima jumătate a anilor 1990. În publicațiile din perioada sovietică, mecanisme similare au fost numite redefinire sau redefinire , operațiuni suprapuse .

Motive pentru

Uneori este nevoie de a descrie și de a aplica operațiuni la tipurile de date create de programator care sunt echivalente ca semnificație cu cele deja disponibile în limbaj. Un exemplu clasic este biblioteca pentru lucrul cu numere complexe . Ele, ca și tipurile numerice obișnuite, suportă operații aritmetice și ar fi firesc să creăm pentru acest tip de operație „plus”, „minus”, „înmulțire”, „împărțire”, notându-le cu aceleași semne de operație ca și pentru alte numere. tipuri. Interdicția utilizării elementelor definite în limbaj obligă la crearea multor funcții cu nume precum ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat și așa mai departe.

Când operațiunile de aceeași semnificație sunt aplicate operanzilor de diferite tipuri, acestea sunt forțate să fie denumite diferit. Incapacitatea de a folosi funcții cu același nume pentru diferite tipuri de funcții duce la nevoia de a inventa nume diferite pentru același lucru, ceea ce creează confuzie și poate duce chiar la erori. De exemplu, în limbajul clasic C, există două versiuni ale funcției de bibliotecă standard pentru găsirea modulului unui număr: abs() și fabs() - prima este pentru un argument întreg, a doua pentru unul real. Această situație, combinată cu verificarea slabă de tip C, poate duce la o eroare greu de găsit: dacă un programator scrie abs(x) în calcul, unde x este o variabilă reală, atunci unii compilatori vor genera cod fără avertisment care va convertiți x într-un număr întreg prin eliminarea părților fracționale și calculați modulul din întregul rezultat.

În parte, problema este rezolvată prin intermediul programării obiectelor - atunci când noi tipuri de date sunt declarate ca clase, operațiunile asupra acestora pot fi formalizate ca metode de clasă, inclusiv metode de clasă cu același nume (deoarece metodele din clase diferite nu trebuie să aibă nume diferite), dar, în primul rând, un astfel de mod de proiectare a operațiunilor pe valori de diferite tipuri este incomod și, în al doilea rând, nu rezolvă problema creării de noi operatori.

Instrumentele care vă permit să extindeți limbajul, să îl completați cu noi operații și construcții sintactice (și supraîncărcarea operațiilor este unul dintre astfel de instrumente, alături de obiecte, macro-uri, funcționale, închideri) îl transformă într-un metalimbaj  - un instrument pentru descrierea limbilor concentrat pe sarcini specifice. Cu ajutorul acestuia, este posibil să construiți o extensie de limbaj pentru fiecare sarcină specifică care este cea mai potrivită pentru aceasta, ceea ce va permite descrierea soluției sale în forma cea mai naturală, mai ușor de înțeles și mai simplă. De exemplu, într-o aplicație de supraîncărcare a operațiilor: crearea unei biblioteci de tipuri matematice complexe (vectori, matrice) și descrierea operațiilor cu acestea într-o formă naturală, „matematică”, creează un „limbaj pentru operații vectoriale”, în care complexitatea calculele este ascunsă și este posibil să se descrie soluția problemelor în termeni de operații vectoriale și matrice, concentrându-se pe esența problemei, nu pe tehnică. Din aceste motive, astfel de mijloace au fost odată incluse în limba Algol-68 .

Mecanism de supraîncărcare

Implementare

Supraîncărcarea operatorului implică introducerea a două caracteristici interdependente în limbaj: capacitatea de a declara mai multe proceduri sau funcții cu același nume în același domeniu și capacitatea de a descrie propriile implementări ale operatorilor binari (adică semnele operațiunilor, scris de obicei în notație infixă, între operanzi). Practic, implementarea lor este destul de simplă:

Supraîncărcarea operatorului în C++

Există patru tipuri de supraîncărcare a operatorului în C++:

  1. Supraîncărcarea operatorilor obișnuiți + - * / % ˆ & | ~ ! = < > += -= *= /= %= ˆ= &= |= << >> >>= <<= == != <= >= && || ++ -- , ->* -> ( ) <=> [ ]
  2. Operatori de conversie de tip supraîncărcare
  3. Supraîncărcarea operatorilor de alocare „nou” și „ștergere” pentru obiectele din memorie.
  4. Supraîncărcarea literalelor "" operator
Operatori obișnuiți

Este important să rețineți că supraîncărcarea îmbunătățește limba, nu schimbă limba, așa că nu puteți supraîncărca operatorii pentru tipurile încorporate. Nu puteți modifica precedența și asociativitatea (de la stânga la dreapta sau de la dreapta la stânga) operatorilor. Nu puteți să vă creați proprii operatori și să supraîncărcați unii dintre cei încorporați: :: . .* ?: sizeof typeid. De asemenea, operatorii && || ,își pierd proprietățile unice atunci când sunt supraîncărcați: lenea pentru primele două și prioritate pentru o virgulă (ordinea expresiilor între virgule este strict definită ca fiind asociată stânga, adică de la stânga la dreapta). Operatorul ->trebuie să returneze fie un pointer, fie un obiect (prin copiere sau referință).

Operatorii pot fi supraîncărcați atât ca funcții autonome, cât și ca funcții membre ale unei clase. În al doilea caz, argumentul din stânga al operatorului este întotdeauna *this obiect. Operatorii = -> [] ()pot fi supraîncărcați doar ca metode (funcții membre), nu ca funcții.

Puteți face scrierea codului mult mai ușoară dacă supraîncărcați operatorii într-o anumită ordine. Acest lucru nu numai că va accelera scrierea, dar vă va scuti și de la duplicarea aceluiași cod. Să luăm în considerare o supraîncărcare folosind exemplul unei clase care este un punct geometric într-un spațiu vectorial bidimensional:

classPoint _ { int x , y ; public : Punct ( int x , int xx ) : x ( x ), y ( xx ) {} // Constructorul implicit a dispărut. // Numele argumentelor constructorului pot fi aceleași cu numele câmpurilor de clasă. }
  • Operatori de copiere și mutare de atribuire operator=
    Este demn de luat în considerare că implicit C++ creează cinci funcții de bază în plus față de constructor. Prin urmare, supraîncărcarea de copiere și mutare a operatorilor de atribuire este cel mai bine lăsată la compilator sau implementată folosind expresia Copy-and-swap .
  • Operatori aritmetici combinați += *= -= /= %=etc.
    Dacă dorim să implementăm operatori aritmetici binari obișnuiți, va fi mai convenabil să implementăm mai întâi acest grup de operatori.Point & Point :: operator += ( const Point & rhs ) { x += rhs . x ; y += rhs . y ; returneaza * asta ; }
Operatorul returnează o valoare prin referință, aceasta vă permite să scrieți astfel de construcții:(a += b) += c;
  • Operatori aritmetici + * - / %
    Pentru a scăpa de repetarea codului, să folosim operatorul nostru combinat. Operatorul nu modifică obiectul, deci returnează un nou obiect.const Point Point :: operator + ( const Point & rhs ) const { return Point ( * this ) += rhs ; }
Operatorul returnează o valoare const. Acest lucru ne va proteja de a scrie construcții de acest fel (a + b) = c;. Pe de altă parte, pentru clasele care sunt scumpe de copiat, este mult mai profitabil să returnezi o valoare dintr-o copie neconstantă, adică : MyClass MyClass::operator+(const MyClass& rhs) const;. Apoi, cu o astfel de înregistrare x = y + z;, va fi apelat constructorul de mutare, nu constructorul de copiere.
  • Operatori unari aritmetici Operatorii + -
    unari plus și minus nu iau argumente atunci când sunt supraîncărcați. Ele nu schimbă obiectul în sine (în cazul nostru), ci returnează un nou obiect modificat. De asemenea, ar trebui să le supraîncărcați dacă omologii lor binari sunt supraîncărcați.
Punct Punct :: operator + () { returnPoint ( * asta ) ; } Point Point :: operator - () { punct tmp ( * asta ); tmp . x *= -1 ; tmp . y *= -1 ; return tmp ; }
  • Operatori de comparație == != < <= > >=
    Primul lucru de făcut este să supraîncărcați operatorii de egalitate și inegalitate. Operatorul de inegalitate va folosi operatorul de egalitate.
bool Point :: operator == ( const Point & rhs ) const { return ( this -> x == rhs . x && this -> y == rhs . y ); } bool Point :: operator != ( const Point & rhs ) const { intoarce- te! ( * asta == rhs ); } În continuare, operatorii < și > sunt supraîncărcați și apoi omologii lor nestrict, folosind operatorii supraîncărcați anterior. Pentru punctele din geometrie, o astfel de operație nu este definită, așa că în acest exemplu nu are rost să le supraîncărcați.
  • Operatori pe biți <<= >>= &= |= ^= и << >> & | ^ ~
    Sunt supuși acelorași principii ca și operatorii aritmetici. În unele clase, utilizarea unei măști de bit va fi utilă std::bitset. Notă: operatorul & are o contrapartidă unară și este folosit pentru a lua o adresă; de obicei nu este supraîncărcat.
  • Operatori logici && ||
    Acești operatori își vor pierde proprietățile unice de lene când sunt supraîncărcați.
  • Increment și decrement ++ --
    C++ vă permite să supraîncărcați atât postfix, cât și prefix, increment and decrement. Luați în considerare o creștere:
Point & Point :: operator ++ () { // prefix x ++ ; y ++ ; returneaza * asta ; } Point Point :: operator ++ ( int ) { //postfix Point tmp ( x , y , i ); ++ ( * aceasta ); return tmp ; } Rețineți că operatorul funcției membru ++(int) ia o valoare de tip int, dar acest argument nu are un nume. C++ vă permite să creați astfel de funcții. Îi putem da (argumentului) un nume și crește valorile punctelor cu acest factor, totuși, sub formă de operator, acest argument va fi implicit la zero și poate fi apelat doar în stil funcțional:A.operator++(5);
  • Operatorul () nu are restricții privind tipul de returnare și tipurile/numărul de argumente și vă permite să creați functori .
  • Un operator care transmite o clasă fluxului de ieșire. Implementat ca o funcție separată, nu o funcție de membru. În clasă, această funcție este marcată ca prietenoasă.friend std::ostream& operator<<(const ostream& s, const Point& p);

Alți operatori nu sunt supuși niciunui ghid general privind supraîncărcarea.

Conversii de tip

Conversiile de tip vă permit să specificați regulile de conversie a clasei noastre în alte tipuri și clase. De asemenea, puteți specifica specificatorul explicit, care va permite conversia tipului numai dacă programatorul a specificat-o în mod explicit (de exemplu , static_cast<Point3>(Point(2,3)); ). Exemplu:

Point :: operator bool () const { returnează acest lucru -> x != 0 || aceasta -> y != 0 ; } Operatori de alocare și dealocare

Operatorii new new[] delete delete[]pot fi supraîncărcați și pot lua orice număr de argumente. În plus, operatorii new и new[]trebuie să ia un argument tip ca prim argument std::size_tși să returneze o valoare de tip void *, iar operatorii trebuie să ia delete delete[]primul void *și să nu returneze nimic ( void). Acești operatori pot fi supraîncărcați atât pentru funcții, cât și pentru clase concrete.

Exemplu:

void * MyClass :: operator new ( std :: size_t s , int a ) { void * p = malloc ( s * a ); dacă ( p == nullptr ) throw "Fără memorie liberă!" ; întoarcere p ; } // ... // Apel: MyClass * p = new ( 12 ) MyClass ;


Litere personalizate

Literele personalizate există încă de la al unsprezecelea standard C++. Literalele se comportă ca funcții obișnuite. Ele pot fi calificative inline sau constexpr . Este de dorit ca literalul să înceapă cu un caracter de subliniere, deoarece poate exista un conflict cu standardele viitoare. De exemplu, literalul i aparține deja numerelor complexe din std::complex.

Literale pot lua doar unul dintre următoarele tipuri: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Este suficient să supraîncărcați literalul numai pentru tipul const char * . Dacă nu se găsește niciun candidat potrivit, atunci va fi apelat un operator de acest tip. Un exemplu de conversie a milelor în kilometri:

constexpr int operator "" _mi ( unsigned long long int i ) { return 1,6 * i ;} constexpr operator dublu "" _mi ( long dublu i ) { return 1,6 * i ;}

Literale șiruri iau un al doilea argument std::size_tși unul dintre primul: const char * , const wchar_t *, const char16_t * , const char32_t *. Literale șir de caractere se aplică intrărilor cuprinse între ghilimele duble.

C++ are încorporat un șir de prefix literal R care tratează toate caracterele citate ca caractere obișnuite și nu interpretează anumite secvențe ca caractere speciale. De exemplu, o astfel de comandă std::cout << R"(Hello!\n)"va afișa Hello!\n.

Exemplu de implementare în C#

Supraîncărcarea operatorului este strâns legată de supraîncărcarea metodei. Un operator este supraîncărcat cu cuvântul cheie Operator, care definește o „metodă operator”, care, la rândul său, definește acțiunea operatorului în raport cu clasa sa. Există două forme de metode operator (operator): una pentru operatorii unari , cealaltă pentru cei binari . Mai jos este forma generală pentru fiecare variantă a acestor metode.

// formă generală a supraîncărcării operatorului unar. public static return_type operator op ( parameter_type operand ) { // operațiuni } // Forma generală de supraîncărcare a operatorului binar. operator public static return_type op ( parameter_type1 operand1 , parameter_type2 operand2 ) { // operații }

Aici, în loc de „op”, este înlocuit un operator supraîncărcat, de exemplu + sau /; iar „return_type” denotă tipul specific de valoare returnat de operația specificată. Această valoare poate fi de orice tip, dar este adesea specificată ca fiind de același tip cu clasa pentru care operatorul este supraîncărcat. Această corelație facilitează utilizarea operatorilor supraîncărcați în expresii. Pentru operatorii unari, operandul indică operandul transmis, iar pentru operatorii binari, același lucru este notat cu „operand1 și operand2”. Rețineți că metodele operatorului trebuie să fie de ambele tipuri, publice și statice. Tipul de operand al operatorilor unari trebuie să fie același cu clasa pentru care operatorul este supraîncărcat. Și în operatorii binari, cel puțin unul dintre operanzi trebuie să fie de același tip cu clasa sa. Prin urmare, C# nu permite supraîncărcarea niciunui operator pe obiecte care nu au fost încă create. De exemplu, alocarea operatorului + nu poate fi suprascrisă pentru elemente de tip int sau string . Nu puteți utiliza modificatorul ref sau out în parametrii operatorului. [unu]

Opțiuni și probleme

Supraîncărcarea procedurilor și funcțiilor la nivelul unei idei generale, de regulă, nu este dificilă nici de implementat, nici de înțeles. Cu toate acestea, chiar și în el există câteva „capcane” care trebuie luate în considerare. Permiterea supraîncărcării operatorului creează mult mai multe probleme atât pentru implementatorul limbajului, cât și pentru programatorul care lucrează în limba respectivă.

Problemă de identificare

Prima problemă este dependența de context . Adică, prima întrebare cu care se confruntă un dezvoltator al unui traducător de limbi care permite supraîncărcarea procedurilor și funcțiilor este: cum să alegeți dintre procedurile cu același nume pe cea care ar trebui aplicată în acest caz particular? Totul este în regulă dacă există o variantă a procedurii, ale căror tipuri de parametri formali se potrivesc exact cu tipurile de parametrii efectivi utilizați în acest apel. Cu toate acestea, în aproape toate limbile, există un anumit grad de libertate în utilizarea tipurilor, presupunând că compilatorul în anumite situații convertește (transmite) în mod automat tipurile de date. De exemplu, în operațiile aritmetice pe argumente reale și întregi, un număr întreg este de obicei convertit automat într-un tip real, iar rezultatul este real. Să presupunem că există două variante ale funcției de adăugare:

int add(int a1, int a2); float add(float a1, float a2);

Cum ar trebui să gestioneze compilatorul expresia y = add(x, i)unde x este de tip float și i este de tip int? Evident, nu există o potrivire exactă. Există două opțiuni: fie y=add_int((int)x,i), fie ca (aici , prima și a doua versiune a funcției sunt notate prin y=add_flt(x, (float)i)nume add_intși respectiv).add_flt

Se pune întrebarea: compilatorul ar trebui să permită această utilizare a funcțiilor supraîncărcate și, dacă da, pe ce bază va alege varianta particulară utilizată? În special, în exemplul de mai sus, traducătorul ar trebui să ia în considerare tipul variabilei y atunci când alege? Trebuie menționat că situația dată este cea mai simplă. Dar sunt posibile cazuri mult mai complicate, care sunt agravate de faptul că nu numai tipurile încorporate pot fi convertite conform regulilor limbajului, ci și clasele declarate de programator, dacă au relații de rudenie, pot fi turnate din unul la altul. Există două soluții la această problemă:

  • Interziceți deloc identificarea incorectă. Solicitați ca pentru fiecare pereche particulară de tipuri să existe o variantă exactă potrivită a procedurii sau operațiunii supraîncărcate. Dacă nu există o astfel de opțiune, compilatorul ar trebui să arunce o eroare. În acest caz, programatorul trebuie să aplice o conversie explicită pentru a transforma parametrii actuali la setul de tipuri dorit. Această abordare este incomodă în limbaje precum C++, care permit o libertate destul de mare în tratarea tipurilor, deoarece duce la o diferență semnificativă în comportamentul operatorilor încorporați și supraîncărcați (operațiile aritmetice pot fi aplicate numerelor obișnuite fără a sta pe gânduri, ci la alte tipuri - numai cu conversie explicită) sau la apariția unui număr mare de opțiuni pentru operațiuni.
  • Stabiliți anumite reguli pentru alegerea „cea mai apropiată potrivire”. De obicei, în această variantă, compilatorul le alege pe acelea dintre variantele ale căror apeluri pot fi obținute din sursă doar prin conversii de tip sigur (informații fără pierderi), iar dacă sunt mai multe, poate alege în funcție de care variantă necesită mai puține. astfel de conversii. Dacă rezultatul lasă mai multe posibilități, compilatorul aruncă o eroare și solicită programatorului să specifice în mod explicit varianta.
Probleme specifice de supraîncărcare a operațiunii

Spre deosebire de proceduri și funcții, operațiunile infixe ale limbajelor de programare au două proprietăți suplimentare care le afectează în mod semnificativ funcționalitatea: prioritatea și asociativitatea , a căror prezență se datorează posibilității de înregistrare „în lanț” a operatorilor (cum să înțelegem a+b*c : cum (a+b)*csau cum a+(b*c)? Expresie a-b+c - aceasta (a-b)+csau a-(b+c)?).

Operațiile încorporate în limbaj au întotdeauna precedență tradițională și asociativitate predefinite. Se pune întrebarea: ce priorități și asociativitate vor avea versiunile redefinite ale acestor operații sau, mai mult, noile operații create de programator? Există și alte subtilități care ar putea necesita clarificări. De exemplu, în C există două forme ale operatorilor de creștere și decrementare ++și -- , prefix și postfix, care se comportă diferit. Cum ar trebui să se comporte versiunile supraîncărcate ale unor astfel de operatori?

Diferite limbi abordează aceste probleme în moduri diferite. Deci, în C++, precedența și asociativitatea versiunilor supraîncărcate ale operatorilor sunt păstrate la fel ca cele ale celor predefiniti în limbaj, iar descrierile de supraîncărcare ale formelor de prefix și postfix ale operatorilor de creștere și decrementare folosesc semnături diferite:

formă de prefix Formular postfix
Funcţie T&operator ++(T&) operator T ++(T &, int)
funcția de membru T&T::operator ++() TT::operator ++(int)

De fapt, operația nu are un parametru întreg - este fictivă și este adăugată doar pentru a face diferența în semnături

Încă o întrebare: este posibil să se permită supraîncărcarea operatorului pentru tipurile de date încorporate și deja declarate? Poate un programator să modifice implementarea operației de adăugare pentru tipul întreg încorporat? Sau pentru biblioteca de tip "matrice"? De regulă, la prima întrebare se răspunde negativ. Modificarea comportamentului operațiunilor standard pentru tipurile încorporate este o acțiune extrem de specifică, a cărei nevoie reală poate apărea doar în cazuri rare, în timp ce consecințele dăunătoare ale utilizării necontrolate a unei astfel de caracteristici sunt greu de prezis chiar și pe deplin. Prin urmare, limbajul fie interzice de obicei redefinirea operațiunilor pentru tipurile încorporate, fie implementează un mecanism de supraîncărcare a operatorului în așa fel încât operațiunile standard pur și simplu nu pot fi depășite cu ajutorul său. În ceea ce privește a doua întrebare (redefinirea operatorilor deja descriși pentru tipurile existente), funcționalitatea necesară este pe deplin oferită de mecanismul de moștenire a clasei și de suprascriere a metodei: dacă doriți să schimbați comportamentul unei clase existente, trebuie să o moșteniți și să o redefiniți. operatorii descriși în acesta. În acest caz, vechea clasă va rămâne neschimbată, cea nouă va primi funcționalitatea necesară și nu vor avea loc coliziuni.

Anunțul de noi operațiuni

Situația cu anunțul de noi operațiuni este și mai complicată. Includerea posibilității unei astfel de declarații în limbă nu este dificilă, dar implementarea acesteia este plină de dificultăți semnificative. Declararea unei noi operații înseamnă, de fapt, crearea unui nou cuvânt cheie în limbaj de programare, complicată de faptul că operațiunile din text, de regulă, pot urma fără separatori cu alte jetoane. Când apar, apar dificultăți suplimentare în organizarea analizatorului lexical. De exemplu, dacă limbajul are deja operațiile „+” și unara „-” (schimbarea semnului), atunci expresia a+-bpoate fi interpretată cu acuratețe ca a + (-b), dar dacă o nouă operație este declarată în program +-, apare imediat ambiguitatea, deoarece aceeași expresie poate fi deja analizată și cum a (+-) b. Dezvoltatorul și implementatorul limbajului trebuie să se ocupe de astfel de probleme într-un fel. Opțiunile, din nou, pot fi diferite: necesită ca toate operațiunile noi să fie cu un singur caracter, postulează că, în cazul oricăror discrepanțe, se alege cea mai „lungă” versiune a operației (adică până la următorul set de caractere citit de către translator se potrivește cu orice operațiune, continuă să fie citit), încearcă să detecteze coliziuni în timpul traducerii și să genereze erori în cazuri controversate... Într-un fel sau altul, limbile care permit declararea de noi operațiuni rezolvă aceste probleme.

Nu trebuie uitat că pentru operațiuni noi se pune și problema determinării asociativității și priorității. Nu mai există o soluție gata făcută sub forma unei operațiuni de limbaj standard și, de obicei, trebuie doar să setați acești parametri cu regulile limbii. De exemplu, faceți toate operațiunile noi asociate stânga și acordați-le aceeași, fixă, prioritate sau introduceți în limbaj mijloacele de specificare a ambelor.

Supraîncărcare și variabile polimorfe

Când operatorii, funcțiile și procedurile supraîncărcate sunt utilizate în limbaje puternic tipizate, în care fiecare variabilă are un tip pre-declarat, este la latitudinea compilatorului să decidă ce versiune a operatorului supraîncărcat să folosească în fiecare caz particular, indiferent cât de complexă. . Aceasta înseamnă că pentru limbajele compilate, utilizarea supraîncărcării operatorului nu reduce în niciun fel performanța - în orice caz, există o operație bine definită sau un apel de funcție în codul obiect al programului. Situația este diferită atunci când este posibil să se utilizeze variabile polimorfe în limbaj - variabile care pot conține valori de diferite tipuri în momente diferite.

Deoarece tipul valorii la care va fi aplicată operația supraîncărcată este necunoscut în momentul traducerii codului, compilatorul este lipsit de posibilitatea de a alege în prealabil opțiunea dorită. În această situație, este forțat să încorporați un fragment în codul obiect care, imediat înainte de efectuarea acestei operațiuni, va determina tipurile de valori din argumente și va selecta dinamic o variantă corespunzătoare acestui set de tipuri. Mai mult, o astfel de definiție trebuie făcută de fiecare dată când se efectuează operația, deoarece chiar și același cod, fiind numit a doua oară, poate fi executat diferit...

Astfel, utilizarea supraîncărcării operatorului în combinație cu variabile polimorfe face inevitabil să se determine în mod dinamic ce cod să apeleze.

Critica

Utilizarea supraîncărcării nu este considerată un avantaj de către toți experții. Dacă supraîncărcarea de funcții și proceduri, în general, nu găsește obiecții serioase (parțial pentru că nu duce la unele probleme tipice „operatorului”, parțial pentru că este mai puțin tentant să o folosești greșit), atunci supraîncărcarea operatorului, ca în principiu, și în special implementările limbajului, este supus unor critici destul de severe din partea multor teoreticieni și practicieni în programare.

Criticii subliniază că problemele de identificare, precedență și asociativitate evidențiate mai sus fac adesea ca tratarea cu operatori supraîncărcați fie inutil de dificilă, fie nefirească:

  • Identificare. Dacă limbajul are reguli stricte de identificare, atunci programatorul este forțat să-și amintească pentru ce combinații de tipuri există operații supraîncărcate și să le transmită manual operanzi. Dacă limbajul permite identificarea „aproximativă”, nu se poate fi niciodată sigur că într-o situație destul de complicată se va executa exact varianta operației pe care programatorul o avea în vedere.
    • „Supraîncărcarea” unei operații pentru un anumit tip este ușor de determinat dacă limbajul acceptă moștenirea sau interfețele ( clase de tip ). Dacă limbajul nu permite acest lucru, atunci este o problemă de design. Deci, în limbajele OOP ( Java , C# ) operatorii de metodă sunt moșteniți din Object, și nu din clasele corespunzătoare (comparație, operații numerice, pe biți etc.) sau din interfețele predefinite.
    • „Identificarea aproximativă” există numai în limbile cu un sistem de tip liber, în care „ capacitatea de a te împușca în picior ” „într-o situație destul de dificilă” este prezentă permanent și fără suprasolicitarea operatorului.
  • Prioritate și asociativitate. Dacă sunt definite rigid, acest lucru poate fi incomod și nu este relevant pentru domeniul subiectului (de exemplu, pentru operațiuni cu seturi, prioritățile diferă de cele aritmetice). Dacă pot fi setate de programator, acesta devine un generator de erori suplimentar (fie și doar pentru că diferite variante ale unei singure operații se dovedesc a avea priorități diferite, sau chiar asociativitate).
    • Această problemă este parțial rezolvată prin definirea de noi operatori (de exemplu, \/atât /\pentru disjuncție , cât și pentru conjuncție ).

Cât de mult poate depăși comoditatea utilizării propriilor operațiuni pe inconvenientul deteriorării gestionabilității programului este o întrebare care nu are un răspuns clar.

Unii critici vorbesc împotriva operațiunilor de supraîncărcare, bazate pe principiile generale ale teoriei dezvoltării software și ale practicii industriale reale.

  • Susținătorii abordării „puritane” a construcției limbilor, precum Wirth sau Hoare , se opun supraîncărcării operatorilor pur și simplu pentru că se presupune că este ușor de făcut fără. În opinia lor, astfel de instrumente nu fac decât să complice limba și traducătorul, fără a oferi caracteristici suplimentare corespunzătoare acestei complicații. În opinia lor, însăși ideea de a crea o extensie a limbii orientată spre sarcini pare doar atractivă. În realitate, utilizarea instrumentelor de extensie a limbajului face ca programul să fie înțeles doar pentru autorul său - cel care a dezvoltat această extensie. Programul devine mult mai dificil de înțeles și analizat pentru alți programatori, ceea ce face mai dificilă întreținerea, modificarea și dezvoltarea echipei.
  • Se observă că însăși posibilitatea de a utiliza supraîncărcarea joacă adesea un rol provocator: programatorii încep să o folosească oriunde este posibil, ca urmare, un instrument conceput pentru a simplifica și eficientiza programul devine cauza complicației și confuziei sale excesive.
  • Este posibil ca operatorii supraîncărcați să nu facă exact ceea ce se așteaptă de la ei, în funcție de felul lor. De exemplu, a + bde obicei (dar nu întotdeauna) înseamnă același lucru, b + adar «один» + «два»diferă de «два» + «один»în limbile în care operatorul +este supraîncărcat pentru concatenarea șirurilor .
  • Supraîncărcarea operatorului face fragmentele de program mai sensibile la context. Fără a cunoaște tipurile de operanzi implicați într-o expresie, este imposibil de înțeles ce face expresia dacă folosește operatori supraîncărcați. De exemplu, într-un program C++ , un operator <<poate însemna atât o schimbare pe biți, ieșire într-un flux, cât și o schimbare a caracterelor dintr-un șir cu un număr dat de poziții. Expresia a << 1returnează:
    • rezultatul deplasării pe bit a valorii cu aun bit la stânga dacă aeste un număr întreg;
    • dacă a - un șir, atunci rezultatul va fi un șir cu un caracter spațiu adăugat la sfârșit (se va face o deplasare caracter cu caracter cu 1 poziție la stânga), iar în diferite sisteme informatice codul caracterului spațiu Poate diferi;
    • dar dacă aeste un flux de ieșire , aceeași expresie va scoate numărul 1 în fluxul respectiv «1».

Această problemă decurge în mod natural din cele două anterioare. Este ușor de nivelat prin acceptarea acordurilor și cultura generală a programării.

Clasificare

Următoarea este o clasificare a unor limbaje de programare în funcție de faptul dacă permit supraîncărcarea operatorului și dacă operatorii sunt limitați la un set predefinit:

Multi
Operatori

Fără supraîncărcare

Există o supraîncărcare
Doar
predefinite

C
Java
JavaScript
Objective-C
Pascal
PHP
ActionScript
Go

Ada
C++
C#
D
Obiect Pascal
Perl
Python
Ruby
VB.NET
Delphi
Kotlin
Rust
Swift

Macabru

Este posibil
să introduceți noi

ML
Pico
Lisp

Algol 68
Fortran
Haskell
PostgreSQL
Prolog
Perl 6
Seed7
Smalltalk
Julia

Note

  1. Herbert Schildt. Ghidul complet pentru C# 4.0, 2011.

Vezi și