Přetěžování operátorů v programování je jedním ze způsobů, jak implementovat polymorfismus , který spočívá v možnosti současné existence několika různých možností ve stejném rozsahu pro použití operátorů, které mají stejné jméno, ale liší se v typech parametrů, ke kterým jsou přiřazeny. aplikovaný.
Termín " přetížení " je pauzovací papír z anglického slova přetížení . Takový překlad se objevil v knihách o programovacích jazycích v první polovině devadesátých let. V publikacích sovětského období se podobné mechanismy nazývaly redefinice nebo redefinice , překrývající se operace.
Někdy je potřeba popsat a aplikovat operace na datové typy vytvořené programátorem, které jsou svým významem ekvivalentní těm, které jsou již v daném jazyce k dispozici. Klasickým příkladem je knihovna pro práci s komplexními čísly . Stejně jako běžné číselné typy podporují aritmetické operace a bylo by přirozené vytvořit pro tento typ operace „plus“, „mínus“, „násobit“, „dělit“ a označovat je stejnými znaménky operací jako pro jiné číselné typy. Zákaz používání prvků definovaných v jazyce si vynucuje vytvoření mnoha funkcí s názvy jako ComplexPlusComplex, IntegerPlusComplex, ComplexMinusFloat a tak dále.
Když jsou operace stejného významu aplikovány na operandy různých typů, jsou nuceny být pojmenovány odlišně. Nemožnost používat funkce se stejným názvem pro různé typy funkcí vede k nutnosti vymýšlet různé názvy pro stejnou věc, což vytváří zmatek a může dokonce vést k chybám. Například v klasickém jazyce C existují dvě verze standardní knihovní funkce pro zjištění modulu čísla: abs() a fabs() - první je pro celočíselný argument, druhá pro skutečný. Tato situace v kombinaci se slabou kontrolou typu C může vést k těžko dohledatelné chybě: pokud programátor do výpočtu zapíše abs(x), kde x je skutečná proměnná, pak některé kompilátory bez varování vygenerují kód, který převeďte x na celé číslo vyřazením zlomkových částí a z výsledného celého čísla vypočítejte modul.
Částečně je problém vyřešen pomocí objektového programování - když jsou nové datové typy deklarovány jako třídy, lze operace s nimi formalizovat jako metody třídy, včetně metod třídy stejného jména (protože metody různých tříd nemusí mít různá jména), ale za prvé je takový způsob návrhu operací s hodnotami různých typů nepohodlný a za druhé neřeší problém vytváření nových operátorů.
Nástroje, které umožňují jazyk rozšiřovat, doplňovat o nové operace a syntaktické konstrukce (a přetěžování operací je jedním z takových nástrojů, spolu s objekty, makry, funkcionály, uzávěry) z něj dělají metajazyk - nástroj pro popis jazyků zaměřené na konkrétní úkoly. S jeho pomocí je možné pro každý konkrétní úkol sestavit jazykové rozšíření, které je pro něj nejvhodnější, což umožní popsat jeho řešení co nejpřirozenější, nejsrozumitelnější a nejjednodušší formou. Například v aplikaci na přetěžování operací: vytvoření knihovny složitých matematických typů (vektory, matice) a popis operací s nimi v přirozené, „matematické“ podobě, vytváří „jazyk pro vektorové operace“, ve kterém je složitost výpočty jsou skryté a je možné popisovat řešení úloh pomocí vektorových a maticových operací se zaměřením na podstatu problému, nikoli na techniku. Z těchto důvodů byly takové prostředky kdysi zahrnuty do jazyka Algol-68 .
Přetěžování operátorů zahrnuje zavedení dvou vzájemně souvisejících funkcí do jazyka: schopnost deklarovat několik procedur nebo funkcí se stejným názvem ve stejném rozsahu a schopnost popsat vlastní implementace binárních operátorů (tj. znaky operací, obvykle zapsané v infixové notaci, mezi operandy). V zásadě je jejich implementace poměrně jednoduchá:
V C++ existují čtyři typy přetížení operátorů:
Je důležité si pamatovat, že přetížení vylepšuje jazyk, nemění jazyk, takže nemůžete přetěžovat operátory pro vestavěné typy. Nelze změnit prioritu a asociativitu (zleva doprava nebo zprava doleva) operátorů. Nemůžete si vytvořit vlastní operátory a přetížit některé vestavěné: :: . .* ?: sizeof typeid. Operátory také && || ,při přetížení ztrácejí své jedinečné vlastnosti: lenost u prvních dvou a přednost u čárky (pořadí výrazů mezi čárkami je striktně definováno jako zleva asociativní, tedy zleva doprava). Operátor ->musí vrátit buď ukazatel nebo objekt (kopírováním nebo odkazem).
Operátory mohou být přetíženy jako samostatné funkce i jako členské funkce třídy. Ve druhém případě je levým argumentem operátoru vždy *tento objekt. Operátory = -> [] ()lze přetížit pouze jako metody (členské funkce), nikoli jako funkce.
Psaní kódu si můžete značně usnadnit, pokud přetížíte operátory v určitém pořadí. To nejen zrychlí zápis, ale také vás ušetří duplikování stejného kódu. Uvažujme přetížení na příkladu třídy, která je geometrickým bodem ve dvourozměrném vektorovém prostoru:
classPoint _ { int x , y ; veřejnost : Bod ( int x , int xx ) : x ( x ), y ( xx ) {} // Výchozí konstruktor je pryč. // Názvy argumentů konstruktoru mohou být stejné jako názvy polí tříd. }Ostatní operátoři nepodléhají žádným obecným směrnicím o přetížení.
Typ konverzePřevody typů umožňují určit pravidla pro převod naší třídy na jiné typy a třídy. Můžete také zadat explicitní specifikátor, který povolí převod typu pouze v případě, že jej programátor výslovně určil (například static_cast<Point3>(Point(2,3)); ). Příklad:
Bod :: operátor bool () const { vrátit toto -> x != 0 || toto -> y != 0 ; } Alokační a dealokační operátořiOperátory new new[] delete delete[]mohou být přetížené a mohou převzít libovolný počet argumentů. Navíc new и new[]musí operátory vzít argument typu jako první argument std::size_ta vrátit hodnotu typu void *a operátory musí vzít delete delete[]první void *a nevrátit nic ( void). Tyto operátory mohou být přetíženy jak pro funkce, tak pro konkrétní třídy.
Příklad:
void * MyClass :: operátor new ( std :: size_t s , int a ) { void * p = malloc ( s * a ); if ( p == nullptr ) hodit "Žádná volná paměť!" ; vrátit p ; } // ... // Volání: MyClass * p = new ( 12 ) MyClass ;
Vlastní literály existují již od jedenáctého standardu C++. Literály se chovají jako běžné funkce. Mohou to být inline nebo constexpr kvalifikátory . Je žádoucí, aby doslovný text začínal znakem podtržení, protože by mohlo dojít ke konfliktu s budoucími standardy. Například literál i již patří ke komplexním číslům z std::complex.
Literály mohou mít pouze jeden z následujících typů: const char * , unsigned long long int , long double , char , wchar_t , char16_t , char32_t. Literál stačí přetížit pouze pro typ const char * . Pokud nebude nalezen žádný vhodnější kandidát, bude zavolán operátor tohoto typu. Příklad převodu mil na kilometry:
constexpr int operátor "" _mi ( bez znaménka long long int i ) { return 1,6 * i ;} constexpr dvojitý operátor "" _mi ( dlouhé dvojité i ) { return 1,6 * i ;}Řetězcové literály mají druhý argument std::size_ta jeden z prvních: const char * , const wchar_t *, const char16_t * , const char32_t *. Řetězcové literály se vztahují na položky v uvozovkách.
C++ má vestavěný předponový řetězcový literál R , který zachází se všemi znaky v uvozovkách jako s běžnými znaky a neinterpretuje určité sekvence jako speciální znaky. Takový příkaz například std::cout << R"(Hello!\n)"zobrazí Hello!\n.
Přetížení operátora úzce souvisí s přetížením metody. Operátor je přetížen klíčovým slovem Operator, které definuje "metodu operátora", která zase definuje akci operátora s ohledem na jeho třídu. Existují dvě formy operátorových metod (operátor): jedna pro unární operátory , druhá pro binární operátory . Níže je uveden obecný formulář pro každou variantu těchto metod.
// obecná forma přetížení unárního operátora. public static operátor typu return_type op ( operand typu_parametru ) { // operace } // Obecná forma přetížení binárního operátoru. public static return_type operator op ( parametr_type1 operand1 , parameter_type2 operand2 ) { // operations }Zde je místo "op" nahrazen přetížený operátor, například + nebo /; a "return_type" označuje konkrétní typ hodnoty vrácené zadanou operací. Tato hodnota může být libovolného typu, ale často se uvádí, že je stejného typu jako třída, pro kterou je operátor přetížen. Tato korelace usnadňuje použití přetížených operátorů ve výrazech. U unárních operátorů operand označuje předávaný operand a u binárních operátorů se totéž označuje jako "operand1 a operand2". Všimněte si, že operátorské metody musí být obou typů, veřejné i statické. Typ operandu unárních operátorů musí být stejný jako třída, pro kterou je operátor přetěžován. A v binárních operátorech musí být alespoň jeden z operandů stejného typu jako jeho třída. C# tedy neumožňuje přetěžování žádných operátorů na dosud nevytvořených objektech. Například přiřazení operátoru + nelze přepsat u prvků typu int nebo string . V parametrech operátora nemůžete použít modifikátor ref nebo out. [jeden]
Přetěžování postupů a funkcí na úrovni obecné myšlenky zpravidla není obtížné ani implementovat, ani pochopit. I v ní však existují některá „úskalí“, která je třeba vzít v úvahu. Povolení přetížení operátorů vytváří mnohem více problémů jak pro implementátor jazyka, tak pro programátora pracujícího v tomto jazyce.
Problém s identifikacíPrvním problémem je kontextová závislost . Tedy první otázka, se kterou se vývojář jazykového překladače, který umožňuje přetěžování procedur a funkcí, potýká: jak vybrat ze stejnojmenných procedur tu, která by měla být aplikována v tomto konkrétním případě? Vše je v pořádku, pokud existuje varianta postupu, jejíž typy formálních parametrů přesně odpovídají typům skutečných parametrů použitých v tomto volání. Téměř ve všech jazycích však existuje určitá míra volnosti v použití typů za předpokladu, že kompilátor v určitých situacích automaticky bezpečně převádí (přetypuje) datové typy. Například v aritmetických operacích s reálnými a celočíselnými argumenty se celé číslo obvykle automaticky převede na reálný typ a výsledek je reálný. Předpokládejme, že existují dvě varianty funkce add:
int add(int a1, int a2); float add(float a1, float a2);Jak by měl kompilátor zacházet s výrazem y = add(x, i), kde x je typu float a i je typu int? Je zřejmé, že neexistuje přesná shoda. Existují dvě možnosti: buď y=add_int((int)x,i), nebo jako (zde jsou první a druhá verze funkce označeny y=add_flt(x, (float)i)jmény add_intresp .).add_flt
Nabízí se otázka: má překladač umožnit toto použití přetížených funkcí, a pokud ano, na základě čeho zvolí konkrétní použitou variantu? Zejména ve výše uvedeném příkladu by měl překladatel při výběru zvážit typ proměnné y? Nutno podotknout, že daná situace je nejjednodušší. Jsou ale možné mnohem komplikovanější případy, které jsou ztíženy tím, že nejen vestavěné typy lze převádět podle pravidel jazyka, ale také třídy deklarované programátorem, pokud mají příbuzenské vztahy, lze z jeden druhému. Existují dvě řešení tohoto problému:
Na rozdíl od procedur a funkcí mají infixové operace programovacích jazyků dvě další vlastnosti, které významně ovlivňují jejich funkčnost: prioritu a asociativitu , jejichž přítomnost je způsobena možností „řetězového“ záznamu operátorů (jak rozumět a+b*c : jak (a+b)*cnebo jak a+(b*c)Výraz a-b+c – toto (a-b)+cnebo a-(b+c)?).
Operace zabudované do jazyka mají vždy předem definovanou tradiční prioritu a asociativitu. Nabízí se otázka: jaké priority a asociativitu budou mít předefinované verze těchto operací, nebo navíc nové operace vytvořené programátorem? Existují další jemnosti, které mohou vyžadovat objasnění. Například v C existují dvě formy operátorů inkrementace a dekrementace ++a -- , prefix a postfix, které se chovají odlišně. Jak se mají chovat přetížené verze takových operátorů?
Různé jazyky řeší tyto problémy různými způsoby. Takže v C++ je priorita a asociativita přetížených verzí operátorů zachována stejně jako u předdefinovaných operátorů v jazyce a přetížené popisy prefixových a postfixových forem inkrementačních a dekrementačních operátorů používají různé signatury:
předponový formulář | Postfixový formulář | |
---|---|---|
Funkce | T&operátor ++(T&) | Operátor T ++(T &, int) |
členská funkce | T&T::operátor ++() | TT::operátor ++(int) |
Operace ve skutečnosti nemá celočíselný parametr – je fiktivní a přidává se pouze proto, aby se změnily podpisy
Ještě jeden dotaz: je možné povolit přetěžování operátorů pro vestavěné a pro již deklarované datové typy? Může programátor změnit implementaci operace sčítání pro vestavěný celočíselný typ? Nebo pro typ knihovny "matrix"? Na první otázku se zpravidla odpovídá záporně. Změna chování standardních operací u vestavěných typů je extrémně specifická akce, jejíž skutečná potřeba může nastat jen ve vzácných případech, přičemž škodlivé důsledky nekontrolovaného používání takové funkce je obtížné dokonce plně předvídat. Jazyk proto obvykle buď zakazuje předefinování operací pro vestavěné typy, nebo implementuje mechanismus přetěžování operátorů takovým způsobem, že standardní operace s jeho pomocí prostě nelze přepsat. Pokud jde o druhou otázku (předefinování operátorů již popsaných pro existující typy), potřebnou funkcionalitu plně zajišťuje mechanismus dědičnosti tříd a přepisování metody: pokud chcete změnit chování existující třídy, musíte ji zdědit a předefinovat operátory v něm popsané. V tomto případě zůstane stará třída nezměněna, nová získá potřebnou funkčnost a nedojde ke kolizi.
Oznámení o nových operacíchJeště složitější je situace s vyhlašováním nových provozů. Zahrnout možnost takového prohlášení v jazyce není obtížné, ale jeho implementace je plná značných potíží. Deklarace nové operace je ve skutečnosti vytvořením nového klíčového slova programovacího jazyka, což je komplikované tím, že operace v textu mohou zpravidla následovat bez oddělovačů s jinými tokeny. Když se objeví, vyvstanou další potíže v organizaci lexikálního analyzátoru. Například, pokud jazyk již má operace „+“ a unární „-“ (změna znaménka), pak lze výraz a+-bpřesně interpretovat jako a + (-b), ale pokud je v programu deklarována nová operace +-, okamžitě vzniká nejednoznačnost, protože stejný výraz již lze analyzovat a jak a (+-) b. Vývojář a implementátor jazyka se s takovými problémy musí nějakým způsobem vypořádat. Možnosti mohou být opět různé: požadovat, aby všechny nové operace byly jednoznakové, předpokládejte, že v případě jakýchkoliv nesrovnalostí bude vybrána „nejdelší“ verze operace (tj. dokud nebude další sada znaků přečtena překladač odpovídá jakékoli operaci, pokračuje ve čtení), snažte se detekovat kolize při překladu a generovat chyby v kontroverzních případech... Tak či onak jazyky, které umožňují deklaraci nových operací, tyto problémy řeší.
Nemělo by se zapomínat, že u nových operací je zde také otázka určování asociativnosti a priority. Již neexistuje hotové řešení v podobě standardní jazykové operace a většinou stačí tyto parametry nastavit s pravidly daného jazyka. Například udělejte ze všech nových operací levý asociativní a dejte jim stejnou, pevnou, prioritu nebo zaveďte do jazyka prostředky pro určení obou.
Když se přetížené operátory, funkce a procedury používají v silně typizovaných jazycích, kde má každá proměnná předem deklarovaný typ, je na kompilátoru, aby rozhodl, kterou verzi přetíženého operátoru v každém konkrétním případě použít, bez ohledu na to, jak složitý je. . To znamená, že u kompilovaných jazyků použití přetěžování operátorů nijak nesnižuje výkon – v každém případě je v objektovém kódu programu dobře definovaná operace nebo volání funkce. Jiná situace je, když je možné v jazyce použít polymorfní proměnné - proměnné, které mohou obsahovat hodnoty různých typů v různých časech.
Vzhledem k tomu, že typ hodnoty, na kterou bude přetížená operace aplikována, není v době překladu kódu znám, je kompilátor připraven o možnost vybrat si předem požadovanou možnost. V této situaci je nuceno vložit fragment do kódu objektu, který bezprostředně před provedením této operace určí typy hodnot v argumentech a dynamicky vybere variantu odpovídající této sadě typů. Navíc taková definice musí být provedena pokaždé, když je operace provedena, protože i tentýž kód, který je volán podruhé, může být dobře proveden odlišně ...
Použití přetěžování operátorů v kombinaci s polymorfními proměnnými tedy činí nevyhnutelné dynamicky určovat, který kód se má volat.
Použití přetížení není všemi odborníky považováno za dobrodiní. Pokud přetěžování funkcí a procedur obecně nenachází žádné závažné námitky (částečně proto, že nevede k některým typickým „provozovatelským“ problémům, částečně proto, že je méně lákavé je zneužít), pak přetěžování operátorů, jako v zásadě, a konkrétně implementace jazyka, je vystaven poměrně ostré kritice ze strany mnoha programátorských teoretiků a praktiků.
Kritici poukazují na to, že problémy s identifikací, prioritou a asociativitou nastíněné výše často činí jednání s přetíženými operátory buď zbytečně obtížné, nebo nepřirozené:
Nakolik může pohodlí používání vlastních operací převážit nepohodlí se zhoršenou ovladatelností programu, to je otázka, která nemá jednoznačnou odpověď.
Někteří kritici hovoří proti přetěžování operací, založených na obecných principech teorie vývoje softwaru a skutečné průmyslové praxe.
Tento problém přirozeně vyplývá z předchozích dvou. Snadno se vyrovná přijetím dohod a obecnou kulturou programování.
Níže je uvedena klasifikace některých programovacích jazyků podle toho, zda umožňují přetížení operátorů a zda jsou operátoři omezeni na předdefinovanou sadu:
Mnoho operátorů |
Žádné přetížení |
Dochází k přetížení |
---|---|---|
Pouze předdefinované |
Ada | |
Je možné zavést nové |
Algol 68 |