na obsah klávesové zkratky na hlavní stránku na menu

Přetěžování funkcí a operátorů

Přetěžování v jazyce C++ umožňuje vytvářet syntakticky přehlednější programy. Při správném použití zpřehledňuje kód a šetří čas. V tomto článku si ukážeme, jak se přetěžují funkce, metody a operátory. Též zmíníme klíčové slovo friend.

Přetěžování funkcí a metod

Přetěžování funkcí je nový koncept v jazyce C++. Jde o jednoduchou myšlenku umožnit deklarování funkcí se stejným jménem, které by se chovaly jinak v závislosti na typech vstupních parametrů. Nic takového nebylo možné v jazyce C, kde mohla být funkce daného jména maximálně jedna. V jazyce C++ je možné vytvořit libovolné množství funkcí stejného jména za předpokladu, že budou rozlišitelné. Aby se překladač mohl rozhodnout, kterou funkci použít, vyžaduje odlišnosti v počtu nebo v typech parametrů. Překladači nestačí odlišení jen ve výstupním parametru a nenechá se zmást použitím typedef! Při výběru vhodné funkce překladač postupuje tak, že vybere tu s vhodným počtem parametrů a následně se řídí v pořadí těmito pravidly:

  1. Přesná shoda v typech.
  2. Shoda v typech po rozšíření typů (char a short na int, float na double).
  3. Shoda v typech po implicitním přetypování (např int na double). Všechny konverze jsou považovány za stejně významné, žádná není upřednostněna.

Ukažme si, jak jednoduše lze vytvořit funkci pro zjištění maxima.

// verze funkce max pro typ int
int max (int a, int b);
// verze funkce max pro typ double
double max (double a, double b);
// maximum ze tří hodnot
double max (double a, double b, double c);

Jak vidíme, můžeme bez problémů umístit deklarace stejně pojmenovaných funkcí do jednoho oboru viditelnosti. Zkusme nyní funkce zavolat.

// zavolá se první verze, přesná shoda
int c = max(3, 5);
// zavolá se druhá verze, přesná shoda
int d = max(3.1, 5.0);
// zavolá se třetí verze, ta jediná má tři parametry
int e = max (1, 2.3, 4);
// chyba, překladač nemá vhodnou funkci
int f = max (3, "ahoj");

Představme si, co by se stalo, kdybychom zavolali funkci max() s těmito parametry.

double c = max(3.1, 5);

V tuto chvíli bude překladač zmaten, neboť má dvě možnosti, jak postupovat. Buď přetypuje 3.1 na int, nebo naopak přetypuje 5 na double. Vždy právě jeden parametr se přesně shoduje. Návratový typ, jak víme, při rozhodování nerozhoduje. Jelikož překladač neví, bude si stěžovat na zmatenost.

main.cpp:26:21: error: call of overloaded ‘max(double, int)’ is ambiguous
main.cpp:3:5: note: candidates are: int max(int, int)
main.cpp:9:8: note:                 double max(double, double)

S přetěžováním metod jsme se již setkali v článku o konstruktorech. Přetěžování metod se nijak neliší od přetěžování funkcí. Konec konců metody jsou jen speciálně umístěné funkce, proto pro jejich přetěžování platí bez výhrad výše uvedená pravidla.

Přetěžování operátorů

V jazyce C existovalo velké množství operátorů, které měly napevno definovanou roli. Operátor bylo možné použít na kompatibilní typy, ale nedalo se měnit jeho chování. Jelikož se na operátor dá pohlížet jako na speciální funkci (např. operátor + lze chápat jako funkci int +(int a, int b)) a jazyk C++ umožňuje přetěžovat funkce, nic nám nebrání v tom přetížit operátor. Nutno však podotknout, že jazyk C++ nedovoluje vytvářet zcela nové operátory, ani měnit aritu a prioritu stávájících! Také je zakázáno přetěžovat operátor tečka (.), čtyřtečka (::) a ternární operátor (?:). Ukažme si nyní, jak se dá přetížit operátor sčítání dvou čísel.

int operator + (int a, int b)
{
  // operátor se bude chovat jako rozdíl, což není dobrý nápad
  return a - b;
}

Deklarace operátoru připomíná deklaraci funkce s tím rozdílem, že před název operátoru dáváme klíčové slovo operator. V příkladu výše jsme přetížili operátor sčítání tak, aby se choval jako operátor odčítání. To není vůbec dobrý nápad. Přetížený operátor může mnohdy usnadnit práci a při mnoha příležitostech je použití přetěžování logické (např. sčítání matic), nicméně musí mít takový úkon smysl a neměl by uživatele kódu nijak mást.

Operátor jako metoda

Operátor může být i součástí třídy jako obyčejná metoda. Představme si, že bychom chtěli napsat třídu reprezentující matici reálných čísel. Pak bude nanejvýš vhodné přetížit standardní aritmetické operátory jako je +, -, +=, -=, * pro násobení vektorem a další. Každý uživatel takové třídy bude očekávat, že matice půjde bez problému sečíst prvek po prvku, čili taková úprava standardních operátorů je vhodná. Operátor jako metoda má však od funkčních operátorů jednu odlišnost. Ta vychází z přítomnosti parametru this, který jsme zmínili v úvodu ke třídám. Z toho tedy vyplývá, že každý operátor ve třídě bude mít o parametr méně a že vždy jako levý operand (popř jediný v případě unárních operátorů) bude přebírat instanci třídy. Ukažme si deklarování operátoru pro hypotetickou třídu Matice.

class Matice
{
  private:
  //pole dat matice
  double *data;
  public:
  // ...
  Matice operator + (const Matice &m) const;
};

// ...
Matice m1, m2, m3;
m3 = m1 + m2;

Všimněme si, že přestože se jedná o binární operátor, předáváme mu pouze jeden parametr. Tím druhým (vlastně prvním při řazení zleva) je ukazatel this. Když se bude vyhodnocovat součet matic m1 a m2, tak se zavolá operátor + jako m1.+(m2), tedy operátor jako metoda bude přidružen k instanci m1, a ta se tak správně stane levým operandem. Toto chování má ovšem jeden vážný nedostatek a to vynucení pořadí operandů. Jinými slovy nelze např. deklarovat operátor pro násobení číslem zleva ve třídě Matice, protože levým operandem musí být vždy instance Matice. Toto lze obejít použitím funkčních operátorů, u kterých je pořadí parametrů libovolně volitelné.

Pokusme se nyní implementovat tělo výše uvedeného operátoru.

Matice Matice::operator + (const Matice &m) const
{
  Matice ret;
  for(int i = 0; i < N; i++)
  {
    ret.data[i] = data[i] + m.data[i];
  }
  return ret;
}

Zde je nutné zmínit několik poznámek. Za prvé je vidět, že stejně jako u kopírovacího konstruktoru můžeme libovolně sahat na soukromé členy všech instancí, neboť jde vždy o stejnou třídu, do které operátor přísluší. Dále je zajímavé, že vytváříme jakousi třetí matici, do které ukládáme výsledek sčítání a ten posléze vracíme. Je to z toho důvodu, že při sčítání dvou matic se ani jedna z nich nemění. Neočekáváme, že po provedení součtu bude sčítanec mít jinou hodnotu. Proto také je vstupní parametr definován jako konstantní a celý operátor je konstantní metoda. Zároveň je jasné, že tento součtový operátor musí vracet parametr hodnotou a ne referencí (důvod je stejný jako v případě funkce proved() v sekci kopírovací konstruktor). Za povšimnutí též stojí umístění kvalifikace, která se dává před slovo operator.

Klíčové slovo friend

Již jsme zmínili, že deklarujeme-li operátor jako metodu, pak je vynucené pořadí operátorů. Pokud bychom chtěli násobit matici číslem, můžeme jako metodu implementovat pouze násobení zprava. Víme, že při použití funkcí nejsme omezeni pořadím parametrů, problém ovšem spočívá v tom, že taková funkce nebude mít přístup k soukromým členům třídy. Jedno z řešení spočívá v přidání veřejné metody, která zpřístupní data matice. Dalším a lepším způsobem je implementovat jako metodu operátor *=, který nepotřebuje měnit pořadí parametrů. S pomocí tohoto operátoru a kopírovacího konstruktoru pak snadno vytvoříme tělo funkčního operátoru násobení.

Matice operator * (double a, const Matice &m)
{
  // vytvoříme pomocnou matici a zkopírujeme obsah m
  Matice ret(m);
  // použijeme existující operátor
  ret *= a;
  // vrátíme výsledek
  return ret;
}

Třetí možností je použít klíčové slovo friend. To umožňuje obejít zapouzdření a zpřístupnit soukromá data třídy externím funkcím a třídám. Jde sice o porušení jednoho ze základních pilířů objektového programování, ale někdy se v omezené míře může hodit.

Klíčové slovo friend se zapisuje před deklaraci funkce nebo třídy (u třídy zmiňujeme její jméno, tedy např. friend Vektor;), které chceme poskytnout dodatečná práva. Tato deklarace musí být v těle třídy, pro kterou práva udělujeme. Kdybychom tedy chtěli poskytnout přístup operátoru *, provedli bychom to následovně.

class Matice
{
  private:
  double *data;
  // ...
  public:
  // ...
  
  friend Matice operator * (double a, const Matice &m);
};

// zde není kvalifikace, je to funkce, nikoliv metoda
Matice operator * (double a, const Matice &m)
{
  // toto teď můžeme provést
  double c = m.data[0];
  // ...
}

Takto deklarovaný operátor je stále funkcí, která nemá žádný přímý vztah ke třídě Matice. To znamená, že musí stále mít dva vstupní parametry a v definici není žádná kvalifikace! Umístění friend pouze zajistí, že tento operátor se může dotázat na data uložená v poli data.