Image
16.6.2016 0 Comments

C++ / Prekrývanie funkcií / 20. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

V závere predošlej časti sme si sľúbili, že sa v tomto pokračovaní budeme zaoberať prekrývaním štandardných operátorov pre objektové údajové typy. Tento sľub aj splníme, ale budeme hovoriť o prekrývaní vo všeobecnom zmysle, t. j. o prekrývaní funkcií ako takých. Iste, čo -  to sme si už o možnosti existencie funkčných homoným v minulosti povedali, ale až teraz vďaka znalostiam tried a práce s nimi môžeme celú problematiku zosumarizovať.

Základné informácie

Prekrývanie funkcií – čo to vlastne znamená? O dvoch (a viacerých) funkciách hovoríme, že sa prekrývajú, ak majú rovnaké meno, ale rôznu deklaráciu, resp. rôznu funkčnú signatúru. Signatúra každej funkcie je daná počtom a typmi jej formálnych argumentov a typom návratovej hodnoty. Na základe počtu a typov skutočných argumentov prekladač pri použití volania funkcie vyberie jej správnu verziu. Príklad:

int max(int, int);
double max(double, double);
 
int i = max(1, 2);
double d = max(3.0, 4.0);

V prvom prípade sa zavolá funkcia int max(int, int), v druhom, naopak, funkcia double max(double, double).

Funkčné homonymá, ako sa prekrytým funkciám niekedy hovorí, musia mať svoje signatúry „dostatočne“ rozdielne. Čo znamená dostatočne, to si hneď vysvetlíme. Prekladač musí pri určovaní správnej verzie funkcie pre každý skutočný argument určiť jeho typ, na základe ktorého bude vyberať, ktorú prekrytú funkciu použije.

Majme ľubovoľný typ T. Dve funkcie, ktorých argument(y) sa líšia iba v tom, že jeden je typu T a druhý typu T&, nemôžu mať rovnaké meno, pretože oba typy T aj T& sú inicializované rovnakou množinou hodnôt a prekladač medzi nimi nedokáže rozlišovať:

void f(double);
void f(double&);  // chyba

Keby sme funkciu f() zavolali napríklad takto:

double d = 1.234;
f(d);

prekladač nemá ako zistiť, ktorú verziu f() vlastne voláme a či má argument d odovzdať hodnotou alebo odkazom.

Podobne sa dve prekryté funkcie nesmú líšiť iba tým, že jedna má argument typu T a druhá typu const T, resp. volatile T. Dôvod je rovnaký ako v predchádzajúcom prípade: všetky tri variácie sú inicializované rovnakou množinou hodnôt (nezabúdajme, že premennú typu const T, resp. volatile T môžeme bez problémov inicializovať premennou typu T a naopak). Navyše špecifikácia formálneho argumentu ako const/volatile hovorí len o tom, že chceme, aby sa daný argument bral v tele funkcie ako konštantná/volatile lokálna premenná nezávisle od toho, akou hodnotou bol inicializovaný.

Je však možné, aby sa dve prekryté funkcie líšili tak, že jedna bude mať argument typu T& a druhá typu const T&, resp. volatile T&. V takomto prípade sa skutočný argument odovzdáva odkazom. Prekladač vie, či je tento argument konštantný/volatile, a príslušný variant funkcie vyberie podľa toho. Napríklad majme takýto kód:

void f(int&);
void f(const int&);
 
int a = 1;
const int b = 2;
volatile int c = 3;
 
f(a);   // OK
f(b);   // OK
f(c);   // chyba

Pri prvom volaní funkcie f() prekladač vyberie verziu f(int&), pri druhom verziu f(const int&) (oboje podľa typu premenných a, resp. b), pri treťom volaní by mal ohlásiť chybu – neexistenciu príslušnej funkcie. Niektoré staršie prekladače však budú hlásiť niečo ako nejednoznačnosť pri výbere funkcie. Premennou c typu volatile int môžeme totiž rovnako dobre inicializovať formálny argument typu int& aj typu const int&,  takže prekladač nedokáže určiť, ktorú verziu funkcie f() chceme volať. Podobná situácia nastane v prípade, že definujeme druhú verziu funkcie f() s argumentom typu volatile int& – prekladač ohlási chybu pri volaní f(b). Ak chceme eliminovať takéto nejednoznačnosti, musíme definovať tri verzie f(), každú pre príslušný špecifikátor.

Všetko, čo sme uviedli v predchádzajúcom odseku, platí rovnakým spôsobom pre typy T*, const T* a volatile T*.

Funkcie, ktoré sa líšia len typom návratovej hodnoty, nesmú byť prekryté. Je to triviálne, pretože stačí nepoužiť návratovú hodnotu funkcie a prekladač nemá ako zistiť, ktorú verziu voláme.

Trieda nesmie mať dve členské funkcie, líšiace sa len v tom, že jedna je statická a druhá nie. Opäť totiž neexistuje spôsob, ako ich odlíšiť v prípade, že ich voláme prostredníctvom operátora . alebo -> nad inštanciou tejto triedy.

Typy, definované pomocou špecifikátora typedef, nie sú samostatnými typmi, ale len synonymami alebo aliasmi pre už existujúce typy. Preto dve funkcie, líšiace sa len takýmto spôsobom, nemôžu mať rovnaké meno:

typedef char* pchar;
 
void f(char*);
void f(pchar);  // chyba

Typy, definované ako enumerácie, však už sú samostatnými typmi, a preto môžu byť použité na rozlišovanie funkčných homoným:

enum E { a, b, c };
 
void f(int i);
void f(E i);  // OK

Isto si spomínate, že pri deklarácii ukazovateľov ako argumentov funkcií môžeme použiť dvojakú syntax – zápis T[] je ekvivalentný zápisu T*. Preto funkcie, ktorých argumenty sa líšia len takýmto spôsobom, nemôžu byť prekryté. Pri viacrozmerných poliach sa samozrejme toto pravidlo vzťahuje len na prvý rozmer, ostatné rozmery sú pri rozlišovaní typov signifikantné:

void f(char*);
void f(char[]);   // chyba
void f(char[10]); // chyba
 
void g(char(*)[5]);
void g(char[3][5]);  // chyba
void g(char(*)[10]); // OK

Pozor ale na správnu deklaráciu; ako vidíme v poslednom príklade, ekvivalentom typu char[3][5] (pole troch päťprvkových znakových polí) je typ char(*)[5] (ukazovateľ na päťprvkové znakové pole) a nie typ char*[5] (pole piatich ukazovateľov na char, inak tiež reprezentované typom char** – ukazovateľom na ukazovateľ na char).

Základným predpokladom pre prekrývanie dvoch funkcií je ich existencia v rovnakom rozsahu platnosti. Už minule sme si spomínali, že členská funkcia odvodenej triedy s rovnakým menom ako funkcia základnej triedy túto zdedenú funkciu neprekrýva, ale ju namiesto toho skrýva. (Napohľad ide o hru so slovíčkami, ale to je len určitá neobratnosť slovenčiny pri preklade z angličtiny – termín prekrývanie je ekvivalentom anglického overloading, ktoré sa bežne prekladá ako preťažovanie, čo však podľa mňa nevystihuje podstatu celého mechanizmu. Problém vzniká pri snahe o preklad anglického overriding, ktoré opisuje existenciu virtuálnych funkcií s rovnakým menom, ktoré majú rovnakú signatúru a pri volaní sa rozlišujú až za behu programu podľa skutočného typu inštancie, nad ktorou boli vyvolané. Pre tento jav som v predošlých častiach použil výraz maskovanie. Tretí výraz, prekladaný ako zakrývanie či skrývanie, je ekvivalentom anglického hiding. Koniec jazykového okienka.) Majme príklad:

class B
{
public:
  void f(int);
};
 
class D : public B
{
public:
  void f(char*);
};

Keďže obe funkcie f() sú každá v inom rozsahu platnosti, nie sú prekryté. Funkcia D::f() zakrýva funkciu B::f(), ktorá je dostupná len pod svojím kvalifikovaným menom:

D* pd = new D;
pd->f(5);      // chyba
pd->B::f(5);   // OK
pd->f("foo");  // OK

Dve členské funkcie v tej istej triede môžu byť prekryté, pretože sa nachádzajú v rovnakom rozsahu platnosti. Každá z oboch funkcií dokonca môže mať rozdielne prístupové práva:

class C
{
private:
  void f(int);
public:
  void f(double);
};

Párovanie argumentov

Predstavme si, čo treba urobiť pri výbere správnej verzie prekrytej funkcie. Jediné, čo má prekladač k dispozícii, je meno prekrytej funkcie a počet a typy skutočných argumentov. Na základe mena funkcie určí množinu prekrytých funkcií, z ktorých si bude v ďalších krokoch vyberať.

Prekladač sa snaží pre každý skutočný argument vybrať takú množinu funkcií, ktoré vykazujú najlepšiu zhodu, čo sa týka typu formálneho argumentu na danej pozícii. Prienik týchto množín určuje funkciu, ktorá sa zavolá. Ak sa stane, že prienik množín obsahuje viac ako jednu funkciu, prekladač ohlási chybu nejednoznačnosti volania. V prípade, že prienikom je prázdna množina, dôjde takisto k chybe, spôsobenej neexistenciou vhodného funkčného homonyma. Vybraná funkcia musí však navyše aspoň pre jeden argument vykazovať striktne lepšiu zhodu ako všetky jej „kolegyne“.

Zostáva osvetliť, čo považujeme za najlepšiu zhodu formálneho a skutočného argumentu. Zrejme úplne najlepší prípad nastane vtedy, keď je skutočný argument rovnakého typu ako príslušný formálny argument. To však vo všeobecnosti nemusí byť pravda a vtedy sa hľadá najlepšia sekvencia konverzií skutočného argumentu na typ formálneho argumentu. Najlepšia preto, lebo konverzných sekvencií môže byť viac, podľa toho, aké typy, konverzné konštruktory či operátory máme v programe definované. Pri hľadaní konverzných sekvencií sa nebude uvažovať taká sekvencia, ktorá obsahuje viac ako jednu používateľskú konverziu (t. j. konverziu pomocou konštruktora alebo pomocou prekrytého operátora pretypovania), a ani sekvencia, z ktorej možno vynechať niektoré kroky bez zmeny sémantiky (napríklad sekvenciu int->float->double možno skrátiť na int->double).

Všetky uvažované sekvencie možno zoradiť do usporiadanej postupnosti, určujúcej ich prioritu pri výbere najlepšej z nich. Táto postupnosť má päť úrovní:

  1. Sekvencie, ktoré obsahujú buď žiadnu, alebo jednu a viac tzv. triviálnych konverzií, sú lepšie ako všetky ostatné. Žiadna konverzia znamená, že ide o presnú zhodu typu formálneho a skutočného argumentu. Za triviálne konverzie sa považujú nasledujúce: z typu T na typ T& a naopak; z typu T[] na typ T; z typu T() na typ T(*)(); z typu T na typ const T, resp. volatile T a konečne z typu T* na typ const T*, resp. volatile T*.
  2. Zo sekvencií iných ako v bode 1 tie, ktoré obsahujú iba celočíselné rozšírenia, konverzie z float na double a triviálne konverzie, sú lepšie ako všetky ostatné.
  3. Zo sekvencií iných ako v bode 2 tie, ktoré obsahujú iba štandardné a triviálne konverzie, sú lepšie ako ostatné. V rámci štandardných konverzií sa považuje za lepšiu konverzia ukazovateľa na odvodenú triedu na ukazovateľ na základnú triedu ako na ukazovateľ na typ void* a ďalej je lepšia konverzia ukazovateľa/referencie na odvodenú triedu na ukazovateľ/referenciu na bližšiu základnú triedu (v zmysle hierarchie tried) ako na vzdialenejšiu.
  4. Zo sekvencií iných ako v bode 3 tie, ktoré obsahujú iba používateľské, štandardné a triviálne konverzie, sú lepšie ako ostatné.
  5. A nakoniec sekvencie, ktoré zahŕňajú konverziu na formálny argument zahrnutý vo výpustke (...), sa považujú za najhoršie.
Priznávam, že predchádzajúce riadky nie sú práve najzrozumiteľnejšie, ale, bohužiaľ, postup pri párovaní argumentov je normatívne daný a nedá sa nijako oklamať. Bolo by asi vhodné uviesť nejaký príklad, ale vzhľadom na variabilitu typov C++ existuje nekonečné množstvo variácií, ktoré všetky nemožno pokryť pár príkladmi, preto skutočne len na ilustráciu:
void f(double, char*);
void f(long, char*);
 
f(1, "foo");
f(100U, "bar");
f(3.5, "xyz");

Prvé volanie funkcie f() vyberie jej verziu f(long, char*), pretože konverzia konštanty 1 typu int na typ long je lepšia ako konverzia na double. Druhé volanie vyberie rovnakú verziu, pretože konverzia konštanty 100 typu unsigned int na typ long je stále lepšia ako konverzia na typ double. Nakoniec tretie volanie vyberie verziu f(double, char*), pretože tentoraz konštanta 3.5 vykazuje presnú zhodu s typom double.

Prekrývanie operátorov

Konečne sa dostávame k trochu zaujímavejšej a opäť objektovej téme - prekrývaniu štandardných operátorov vlastnými, používateľskými verziami. Prekrytie štandardného operátora v podstate modifikuje jeho význam pri použití s vlastnými, objektovými typmi. Prekrývaním operátorov však nemôžeme zmeniť ich význam pre štandardné typy C++, takisto nemôžeme zmeniť ich aritu (t. j. to, či sú unárne, binárne, ternárne), prioritu, asociativitu ani prefixovosť/postfixovosť. Nemôžeme si ani zadefinovať nové operátory (napr. nemôžeme zobrať znak $ a definovať ho ako operátor).

Prekryť môžeme takmer všetky operátory C++ s výnimkou nasledujúcich:

.
.*
::
?:
sizeof

Operátor .* zatiaľ ešte nepoznáme, ale povieme si o ňom na záver tejto časti. Prekryť takisto nemôžeme operátory preprocesora #, ## a defined.

Funkcie, ktorými prekrývame pôvodné definície operátorov, musia mať tvar operator meno( argumenty ), kde meno je symbol príslušného operátora. Počet, poradie a typy argumentov sú špecifické pre jednotlivé operátory.

Prekryté operátorové funkcie musia byť buď členskými funkciami nejakej triedy, alebo musia mať jeden z argumentov typu triedy/enumerácie alebo referencie na triedu/enumeráciu. Členské operátorové funkcie sa dedia bežným spôsobom s výnimkou funkcie operator=(). Nečlenské operátorové funkcie sa často deklarujú ako spriatelené danej triede/triedam.

Štandardné operátory vykazujú určitú ekvivalenciu v použití, napr. výraz ++x je ekvivalentný výrazu x+=1. Táto ekvivalencia nemusí byť dodržaná pre používateľské verzie operátorov, a preto je dobré nespoliehať sa na ňu pri používaní cudzích tried.

V nasledujúcich odsekoch si preberieme jednotlivé typy operátorov a špecifiká ich prekrývania. Väčšinu operátorov budeme dopĺňať do triedy complex, ktorú sme si definovali už minule a ktorá reprezentuje údajový typ komplexné číslo. Tu je jej základná deklarácia:

class complex
{
  double re, im;
public:
  complex(double r = 0.0, double i = 0.0) : re(r), im(i) {}
  complex(complex& c) : re(c.re), im(c.im) {}
  double real() { return re; }
  double imag() { return im; }
};

Unárne operátory

Prefixové unárne operátory môžeme pretypovať buď nestatickou členskou funkciou bez argumentov, alebo nečlenskou funkciou s jedným argumentom. V prvom prípade bude výraz @x pre ľubovoľný unárny operátor @ interpretovaný ako x.operator@(), v druhom prípade ako operator@(x). V prípade existencie oboch verzií sa uplatní klasické rozlišovanie prekrytých funkcií na základe párovania argumentov.

Zaveďme do našej triedy complex unárny operátor -, ktorý bude vracať opačné číslo k danému komplexnému číslo (opačné číslo k číslu a + bi je číslo –abi):

complex complex::operator-()
{ return complex(-re, -im); }

(Nezabudnite doplniť do deklarácie triedy príslušný prototyp!) Všimnite si, že sme návratovú hodnotu našej verzie operátora - deklarovali ako complex. Je dobrým zvykom dodržiavať pravidlá zavedené štandardnými operátormi v tom zmysle, že ak štandardná verzia vracia l-hodnotu, mala by ju vracať aj používateľská verzia, takisto ak štandardná verzia nevracia l-hodnotu, nemala by ju vracať ani používateľská verzia. L-hodnotu obyčajne vracajú tie operátory, ktoré akýmkoľvek spôsobom menia svoje operandy (ako napríklad =, +=, ++ a pod.). V našom príklade vraciame ako návratovú hodnotu dočasnú inštanciu triedy complex, takže ani nemá zmysel vracať ju odkazom.

Vďaka prekrytému operátoru – teraz môžeme získať opačné číslo k nejakému komplexnému číslu veľmi jednoducho:

complex c1(1, 2);
complex c2 = -c1;

Teraz si skúste sami do triedy complex doplniť vlastnú verziu operátora ~, ktorá bude vracať k danému komplexnému číslu číslo k nemu komplexne združené. Ak by ste mali problémy, v predposlednej časti seriálu sme definovali členskú funkciu complex::conj() s rovnakým významom.

Operátory ++ a --

S unárnymi operátormi ++ a -- je to o niečo zložitejšie, pretože oba majú dve verzie – prefixovú a postfixovú. Prefixovú verziu prekrývame buď pomocou členskej funkcie bez argumentov, alebo pomocou nečlenskej funkcie s jedným argumentom typu danej triedy, teda rovnakým spôsobom ako iné unárne operátory. Pri definícii prekrytej postfixovej verzie však musíme doplniť fiktívny argument typu int, ktorý sa nijako nepoužíva, pri volaní má nulovú hodnotu a slúži len na rozlíšenie oboch verzií operátorov.

Prefixová a postfixová štandardná verzia oboch operátorov sa líši aj v tom, že prvá z nich vracia l-hodnotu, zatiaľ čo druhá nie. Ak chceme dodržať túto konvenciu, musíme patrične deklarovať návratovú hodnotu prekrytých verzií. Vo všeobecnosti, ak chceme vracať l-hodnotu, stačí deklarovať návratovú hodnotu ako referenciu. Musíme si však dať pozor na to, aby sme touto referenciou skutočne vracali objekt, nad ktorým pracujeme, a nie nejakú dočasnú lokálnu premennú. V členských funkciách je to jednoduché – aktuálnu inštanciu vrátime výrazom return *this;. V prípade nečlenských funkcií však pozor: ak chceme objekt modifikovať, musí príslušná operátorová funkcia mať za argument referenciu na menený objekt. Túto referenciu potom vrátime bežným spôsobom.

Pre objasnenie si teraz ukážeme na príklade rozdiely medzi jednotlivými verziami. Operátory, ktoré definujeme, budú tak trochu za vlasy pritiahnuté, pretože budú inkrementovať/dekrementovať len reálnu zložku daného komplexného čísla, ale to na celom príklade nič nemení. Najprv teda obe verzie operátora ++ ako členské funkcie:

complex& complex::operator++()
{
  re++;
  return *this;
}
 
complex complex::operator++(int)
{
  complex tmp(*this);
  re++;
  return tmp;
}

Všimnite si, že v prípade postfixovej verzie musíme vytvoriť pomocnú premennú tmp, ako kópiu súčasného stavu, ktorú vrátime po tom, čo modifikujeme objekt *this. Tak je totiž definovaná sémantika postfixového operátora ++.

A tu sú obe verzie operátora --, tentoraz ako nečlenské funkcie:

complex& operator--(complex& c)
{
  c.re--;
  return c;
}
 
complex operator--(complex& c, int)
{
  complex tmp(c);
  c.re--;
  return tmp;
}

Na to, aby prekladač obe funkcie správne preložil, je však potrebné do deklarácie triedy complex doplniť tieto dva riadky:

friend complex& operator--(complex&);
friend complex operator--(complex&, int);

Binárne operátory

Binárne operátory môžeme pretypovať buď nestatickou členskou funkciou s jedným argumentom, alebo nečlenskou funkciou s dvoma argumentmi. V prvom prípade bude výraz x@y pre ľubovoľný binárny operátor @ interpretovaný ako x.operator@(y), v druhom prípade ako operator@(x,y). V prípade existencie oboch verzií sa opäť uplatní rozlišovanie prekrytých funkcií na základe párovania argumentov.

Ako príklad si zavedieme do našej triedy complex operátor * realizujúci bežné násobenie komplexných čísel. Pre zopakovanie: súčin čísel a + bi a c + di je rovný (acbd) + (ad + bc)i:

complex complex::operator*(complex c)
{
  double newre = re * c.re - im * c.im;
  double newim = re * c.im + im * c.re;
  return complex(newre, newim);
}

Premenné newre a newim sme zaviedli iba pre sprehľadnenie zápisu. Všimnite si, že návratová hodnota nášho operátora nie je l-hodnotou, tak ako je to zvykom pri štandardnom operátore *. Argument nášho operátora je odovzdávaný hodnotou. Ak sa chceme vyhnúť v podstate zbytočnému volaniu konštruktora, môžeme ho odovzdať aj odkazom, i keď v tele operátora pôvodný objekt nemeníme. Aby sme však aj v budúcnosti zabránili nechcenej modifikácii pôvodného objektu, zmeníme deklaráciu operátora * takto:

complex complex::operator*(const complex& c)
{ ... }

Takto zabezpečíme, že sa pri vyvolaní operátora * nad dvoma komplexnými číslami nebude zbytočne konštruovať nový objekt, predstavujúci kópiu jedného z operandov, ktorý aj tak slúži iba na čítanie údajových členov. Podobný prístup môžeme použiť pri nečlenskej verzii operátorovej funkcie.

Samozrejme, že nemusíme definovať iba operátor pre násobenie dvoch komplexných čísel. Rovnakým spôsobom môžeme zadefinovať napríklad operátor násobenia komplexného čísla reálnym. Prototyp takej funkcie bude nasledujúci:

complex complex::operator*(double d);

Definíciu tela ponechám na vás. Ale pozor: takto definovaný operátor funguje iba pre násobenie komplexného čísla reálnym sprava. Pokiaľ chceme realizovať aj násobenie zľava, musíme už definovať nečlenskú funkciu s prototypom:

complex operator*(double d, complex c);

Iste ste si všimli, že nám tento mechanizmus umožňuje vytvoriť dva rôzne varianty násobenia komplexného a reálneho čísla podľa ich vzájomného poradia, čím úspešne zrušíme komutatívnosť operátora *. Takýto prístup sa vo všeobecnosti neodporúča.

Medzi binárne operátory, ktoré možno pretypovať uvedeným spôsobom, patria ďalej operátory +, -, /, %, ^, &, |, <, >, <=, >=, ==, !=, &&, ||, <<, >> a operátor , (čiarka). Pozor však pri operátoroch &&, || a čiarka. Ich používateľské verzie totiž nemajú zaručené špeciálne poradie vyhodnocovania operandov!

Na precvičenie si skúste do triedy complex doplniť tie binárne operátory, ktoré majú pre komplexné čísla nejaký praktický význam.

Operátor priradenia

Priraďovací operátor má zvláštne postavenie. Môže byť definovaný iba ako členská funkcia s jediným argumentom, nededí sa a v prípade jeho neexistencie si ho prekladač dokáže doplniť sám. Implicitný operátor priradenia vytvára plytkú kópiu, t. j. kopíruje stav objektu člen po člene, použijúc pre objektové členy ich operátory priradenia. Jeho prototyp bude pre triedu X vyzerať takto:

X& X::operator=(const X&);

V prípade, že trieda obsahuje objektové členy, ktorých priraďovacie operátory neakceptujú konštantné argumenty, mení sa tento prototyp na nasledujúci:

X& X::operator=(X&);

Implicitný operátor priradenia nie je možné vytvoriť v prípade, že trieda obsahuje konštantné členy, referencie alebo objektové členy s privátnym priraďovacím operátorom.

Operátor priradenia sa vyvolá vždy, keď prekladač narazí na výraz x = e, kde x je inštancia objektového typu, e je výraz. Ako už vieme, deklarácia spojená s inicializáciou má za následok vyvolanie kopírovacieho konštruktora, a nie priraďovacieho operátora.

Pre našu triedu complex nemá zmysel deklarovať samostatný priraďovací operátor, úplne postačí ten implicitný. Pre ilustráciu si však ukážeme, ako by taká deklarácia vyzerala:

complex& complex::operator=(const complex& c)
{
  re = c.re;
  im = c.im;
}

Všimnime si, že ak budeme vracať výsledok nie odkazom, ale iba hodnotou, nebude možné reťaziť priraďovacie operátory spôsobom x = y = z.

Operátor volania funkcie

Volanie funkcie v tvare:

výraz ( zoznam-argumentov );

sa považuje za binárny operátor, ktorého prvým operandom je výraz a druhým zoznam-argumentov. Prekryť ho môžeme iba nestatickou členskou funkciou s názvom operator(). Volanie x(arg) je potom ekvivalentné zápisu x.operator()(arg), kde x je inštancia triedy.

Ako príklad si uvedieme triedu matrix, reprezentujúcu maticu údajov. Jej podstatným údajovým členom bude dvojrozmerné pole prvkov typu double. Pre jednoduchosť nebudeme zavádzať komplikované konštruktory s dynamickou alokáciou poľa, ale budeme mať pevne dané rozmery matice. Dôležitý bude totiž v triede matrix prekrytý operátor volania funkcie, ktorý bude slúžiť na prístup k jednotlivým prvkom matice. Jeho dva argumenty budú predstavovať požadované indexy:

const int N = 5;
 
class matrix
{
  double mm[N][N];
public:
  double& operator()(int i, int j)
  { return mm[i][j]; }
};

Pre jednoduchosť netestujeme, či oba indexy nepresahujú povolený rozsah. Zavedený operátor () nám umožňuje pracovať s prvkom mij matice pomocou zápisu m(i, j):

matrix m;
m(3, 2) = 10;

Priradenie je možné, pretože operátor () vracia l-hodnotu (typ double&).

Operátor indexovania

Podobne ako v predchádzajúcom prípade je indexovanie poľa v tvare:

výraz [ výraz ];

považované za binárny operátor. Výraz x[y] je interpretovaný ako x.operator[](y), kde x je inštancia objektového typu. Prekrytý operátor [] musí byť nestatickou členskou funkciou.

Ako príklad si uvedieme triedu vector, čo je vlastne jednorozmerný prípad predchádzajúcej triedy matrix:

class vector
{
  double vv[N];
public:
  double& operator[](int i)
  { return vv[i]; }
};

Podobnosť s triedou matrix je zrejmá. Pre prístup k prvkom vektora však tentoraz používame operátor []. V triede matrix sme museli použiť operátor () z toho dôvodu, že prvky matice sú prístupné pomocou dvoch indexov. Je, samozrejme, možné aj také riešenie, že vytvoríme triedu matrix, ktorá bude obsahovať jednorozmerné pole prvkov typu vector. Potom môžeme prekryť operátor [], ktorý vráti príslušný riadok (či stĺpec – podľa toho, ako maticu definujeme). Na tento výsledok typu vector sa potom aplikuje operátor [] s druhým indexom.

Operátor prístupu k členom triedy

Ďalším špecifickým operátorom je operátor ->. Ten je považovaný za unárny v tom zmysle, že výraz x->m je interpretovaný ako (x.operator->())->m. Operátorová funkcia operator->() musí byť nestatickou členskou funkciou a musí vracať buď ukazovateľ na nejakú triedu, alebo objekt takej triedy, pre ktorú je definovaný operátor -> (resp. referenciu na takúto triedu).

Prekrytý operátor -> sa obyčajne používa pri triedach, ktoré zapuzdrujú nejaké ukazovatele. Typický príklad:

class cptr
{
  complex* p;
public:
  complex* operator->()
  { return p; }
};

Z deklarácie triedy cptr síce nevidieť, prečo by sme nemohli rovno používať typ complex*, ale zmysel táto trieda dostane v okamihu, keď doplníme konštruktor a deštruktor, ktoré budú automaticky alokovať/dealokovať objekt triedy complex. Trieda cptr potom bude predstavovať tzv. automatický ukazovateľ. Takéto ukazovatele sa používajú pomerne často a šablóny pre ne sú dokonca súčasťou štandardnej knižnice C++.

Operátory alokácie a dealokácie

Na alokáciu, resp. dealokáciu dynamických objektov slúžia v C++ operátory new a delete, s ktorými sú zviazané operátorové funkcie operator new() a operator delete(). Tieto funkcie však pracujú trochu ináč, ako je to zvykom pri ostatných prekrytých operátoroch.

Operátor new volá na alokovanie príslušného miesta v pamäti funkciu operator new(). Pre neobjektové typy sa použije jej globálna verzia ::operator new(), pre inštancie triedy X sa použije verzia X::operator new() – samozrejme, len vtedy, ak sme ju definovali. Použitie globálnej verzie aj v prípade objektových typov si môžeme vynútiť pripojením operátora :: pred kľúčové slovo new.

Funkcia operator new() je v prípade objektových typov vždy statická, aj keď takto nie je deklarovaná. Jej návratová hodnota musí byť typu void* a jej prvý argument, predstavujúci veľkosť alokovaného miesta v pamäti, musí byť typu size_t. Prípadné ďalšie argumenty sú voliteľné, pri volaní operátora new ich uvádzame v zátvorke za kľúčovým slovom new (pred prípadným inicializátorom). Klasickým príkladom je definícia takého operátora new, ktorý umiestni nový objekt na nami určené miesto v pamäti:

void* operator new(size_t, void* ptr)
{ return ptr; }

Ak teraz chceme vytvoriť novú premennú typu complex, umiestnenú v nejakej pamäťovej oblasti buf, použijeme takýto zápis:

char* buf[512];
complex* cp = new(buf) complex(1.2, 3.4);

Na takto získaný ukazovateľ však nesmieme použiť štandardný operátor delete, ten by sa totiž snažil danú oblasť pamäte bežným spôsobom dealokovať.

Ak funkcia operator new() nedokáže alokovať dostatočne veľkú oblasť pamäte, vracia 0. Vtedy aj samotný operátor new vracia nulu, ako oznámenie neúspechu pri alokácii.

Podobne operátor delete volá pre dealokáciu obsadenej pamäte funkciu operator delete(). Opäť sa pre neobjektové typy použije jej globálna verzia ::operator delete() a pre inštancie triedy X verzia X::operator delete(), pokiaľ je definovaná. Zápis ::delete si vynúti použitie globálnej verzie aj pre objektové typy.

Aj funkcia operator delete() je statická, jej návratová hodnota musí byť void a jej prvý argument, reprezentujúci ukazovateľ na dealokovanú pamäť, musí byť typu void*. Navyše môžeme pridať druhý argument typu size_t, ktorý bude predstavovať veľkosť dealokovaného objektu. V rámci jednej triedy môže existovať iba jediná funkcia operator delete().

Keďže sú členské funkcie operator new() a operator delete() statické, nemôžu byť virtuálne. Nájdenie ich správnych verzií pre daný objekt je zabezpečené jedinečnosťou konštruktora a virtuálnosťou deštruktora.

Podľa najnovšej normy ANSI C++ môžeme v programe definovať aj vlastnú verziu funkcií pre alokáciu/dealokáciu polí. Tieto funkcie majú názov operator new[](), resp. operator delete[]().

Ukazovatele na členy tried

Dosiaľ sme si nič nehovorili o ukazovateľoch na členy triedy. Tento údajový typ je zvláštnym prípadom klasických ukazovateľov, s tým obmedzením, že nemôže ukazovať na ľubovoľnú premennú, resp. oblasť pamäte, ale len na člena (údajového či funkčného) tej - ktorej triedy. Každý takýto ukazovateľ je zviazaný so „svojou“ triedou a ukazovateľ do triedy A nemôže ukazovať na členy triedy B.

Deklarácia členských ukazovateľov je na prvý pohľad trochu mätúca. Jej všeobecný zápis vyzerá takto:

T meno-triedy :: * cv-kvalifikátory D1

T je bežný špecifikátor typu. Meno-triedy určuje triedu, do ktorej daný ukazovateľ bude ukazovať, cv-kvalifikátory (const a volatile) umožňujú deklarovať ukazovateľ ako konštantný (nie na konštantu!), resp. volatile. D1 je ďalší deklarátor – používame rovnakú schému zápisu ako v časti venovanej deklaráciám. Celý typ deklarovaného identifikátora vnoreného v D1 je „… cv-kvalifikovaný ukazovateľ na člena triedy meno-triedy typu T“.

Ukážme si príklad. Majme jednoduchú triedu:

class C
{
public:
  int a, b;
  int f(double);
};

Teraz môžeme deklarovať napríklad ukazovateľ na celočíselný člen triedy C:

int C::* pi;

a prinútiť ho ukazovať na člen a alebo na člen b triedy C:

pi = &C::a;
pi = &C::b;

Všimnite si, že sme dosiaľ nedeklarovali žiadnu inštanciu triedy C, ukazovateľ pi ukazuje všeobecne na člen a, resp. b triedy C. Podobne môžeme deklarovať ukazovateľ na niektorú členskú funkciu:

int (C::* pf)(double) = &C::f;

Ukazovateľ pf je ukazovateľom na takú členskú funkciu triedy C, ktorá má jediný argument typu double a vracia typ int. Týmto podmienkam vyhovuje členská funkcia C::f(), preto sme aj ukazovateľ na ňu priradili do pf. Keď si dobre preštudujete spôsob deklarácie členských ukazovateľov, všimnete si, že sa vlastne deklarujú ako bežné ukazovatele, len pred hviezdičkou je navyše meno ich „domácej“ triedy so štvorbodkou.

Samozrejme, na to, aby nám členské ukazovatele boli na niečo užitočné, musíme ich dokázať aj dereferencovať. Na to máme k dispozícii dva operátory .* a ->*. Oba sú binárne, infixové, asociujú sa zľava doprava a majú prioritu menšiu než unárne operátory, ale väčšiu než multiplikatívne operátory. Ak ich pravý operand je ukazovateľom na člena triedy C, ich ľavý operand musí byť typu triedy C alebo jej dostupného predka (to pre operátor .*), resp. typu ukazovateľa na triedu C alebo jej dostupného predka (pre operátor ->*).

Použitie oboch operátorov sa už musí vzťahovať na konkrétnu inštanciu objektového typu. Výsledkom aplikácie operátorov je príslušný údajový člen či členská funkcia tej inštancie, na ktorú boli aplikované. Majme deklarovanú triedu C a ukazovatele pi a pf ako predtým:

C c;
c.*pi = 12;
C* pc = &c;
int i = (pc->*pf)(3.14);

V tomto príklade priraďujeme hodnotu 12 tomu údajovému členu inštancie c triedy C, na ktorý ukazuje ukazovateľ pi. Ďalej voláme s parametrom 3.14 tú členskú funkciu, na ktorú ukazuje ukazovateľ pf.

Na ukazovatele do tried sa vzťahuje niekoľko pravidiel. Predovšetkým tieto ukazovatele nemôžu ukazovať na statické členy tried. Ak ukazovateľ ukazuje na funkciu, jeho dereferencovanú hodnotu možno použiť iba na realizáciu funkčného volania. Ukazovateľ na člena základnej triedy možno bez problémov konvertovať na ukazovateľ na člena triedy odvodenej za predpokladu, že ukazovateľ na odvodenú triedu môžeme konvertovať na ukazovateľ na základnú triedu. Inými slovami, ukazovatele na členy tried je dovolené konvertovať presne opačným smerom ako ukazovatele na samotné triedy. Prečo je to tak? Zamyslite sa nad tým.

Na záver

Dnes sme toho prebrali pomerne veľa. Zostáva nám ešte prebrať šablóny, výnimky, niektoré nové črty C++, zavedené normou ANSI C++, a trochu miesta venujeme aj štandardnej knižnici C++. Je možné, že to všetko stihneme ešte v tomto roku. Ale nepredbiehajme.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá