Kurz C++ (16.)

Vítejte u dalšího z kurzů věnovaných jazyku C++. V dnešním dílu dokončíme polymorfizmus ve vztahu k přetěžování operátorů. Další odstavec bude věnován šablonám. Přeji pěkné čtení.

16.1. Polymorfizmus a operátory

    Opět si zopakujeme, že se nedědí přiřazovací operátor operator=(). Operátory new a delete definované jako metody se dědí jako statické metody. Ostatní operátory se dědí a mohou být, podobně jako metody, virtuální. Zavoláním virtuálního operátoru dojde, opět stejně jako u metod, k zavolání operátoru příslušnému k levému operandu operátorové funkce. Ukažme si příklad z naší zoologické zahrady a přetěžme si operátor << tak, aby sloužil pro nakládání zvířat na náklaďák. Je jasné, že různá zvířata se musí nakládat rozdílným způsobem a proto budeme muset v třídě CNakladak přetížit operátory pro všechny druhy zvířat, která budeme chtít převážet. Budeme předpokládat, že na nákladní vůz se vejde vše, co na něj budeme chtít uložit:

Nakladak.h:

#ifndef _NAKLADAK_H_
  #define _NAKLADAK_H_

  class CNakladak {
  private:
      int m_iBenzin;
  public:
      CNakladak& operator<<(CZirafa _zir);
      CNakladak& operator<<(CLev _lev);
  };

#endif

Nakladak.cpp:

#include <iostream.h>

#include "Zivocich.h"
#include "Zirafa.h"
#include "Lev.h"

#include "Nakladak.h"

CNakladak& CNakladak::operator<<(CZirafa _zir)
{
   
// ulozeni zirafy na nakladak
    cout << "Ukladam zirafu na nakladak" << endl;

    return *this;
// vratime tento objekt abychom mohli retezit jako pri cout
}

CNakladak& CNakladak::operator<<(CLev _lev)
{
   
// ulozeni lva na nakladak
    cout << "Ukladam lva na nakladak" << endl;

    return *this;
// vratime tento objekt abychom mohli retezit jako pri cout
}

    Všimněte si, že operátory vrací proud, abychom je mohli řetězit, jak vidíme ve funkci main():

int main(int argc, char* argv[])
{
    CNakladak nakladak;
    CLev lev;
    CZirafa zirafa;

    nakladak << lev << zirafa;
// ulozime je na nakladak

    char c;
    cin >> c;

    return 0;
}

    Zdrojový kód naleznete v sekci Downloads (projekt PrevozZOO1).

    Pokud bychom nyní přidali další zvíře, museli bychom najít třídu CNakladak a do ní pak přidat kód pro uložení tohoto zvířete. Podívejme se na jiný způsob řešení tohoto problému, kde využijeme virtuální funkce:

Nakladak.h:

#ifndef _NAKLADAK_H_
  #define _NAKLADAK_H_

  class CZivocich;   
// Predebezna deklarace

  class CNakladak {
  private:
      int m_iBenzin;   
// Napriklad
  public:
      CNakladak& operator<<(CZivocich& _ziv);
  };

#endif

Nakladak.cpp:

#include "Zivocich.h"
#include "Nakladak.h"

CNakladak& CNakladak::operator<<(CZivocich& _ziv)
{
    // ulozeni zivocicha na nakladak
    return _ziv.Naloz(*this);
}

Zivocich.h:

#ifndef _ZIVOCICH_H_
  #define _ZIVOCICH_H_

  class CNakladak;   
// Predebezna deklarace

  class CZivocich {
  protected:
      int m_dwMaxVek;
      int m_dwVek;
  public:
      CZivocich(int _dwMaxVek, int _dwVek) : m_dwMaxVek(_dwMaxVek), m_dwVek(_dwVek) { ; }

      virtual HledejPotravu() { ; }
      virtual void Zij() { ; }
      virtual CNakladak& Naloz(CNakladak& _nakl) = 0;   
// Ciste virtualni
  };

#endif

Lev.h:

#ifndef _LEV_H_
  #define _LEV_H_

  class CLev : public CZivocich {
  protected:

  public:
      CLev() : CZivocich(15, 0) { ; }

      virtual HledejPotravu();
      virtual void Zij();
      virtual CNakladak& Naloz(CNakladak& _nakl);
  };

#endif

Lev.cpp:

#include <iostream.h>

#include "Nakladak.h"

#include "Zivocich.h"
#include "Lev.h"

CLev::HledejPotravu()
{
    cout << "Lev : hledam nejake maso" << endl;
}

void CLev::Zij()
{
    if(-1 == m_dwVek)
    {
        cout << "Lev : jsem uz po smrti" << endl;
    }
    else
    {
        if(m_dwVek < m_dwMaxVek)
        {
            HledejPotravu();
// Najime se
            m_dwVek++;
// Posuneme o den
            cout << "Lev : mam prave narozeniny " << m_dwVek << endl;
        }
        else
        {
            cout << "Lev : umiram ve veku (" << m_dwVek << ")" << endl;
            m_dwVek = -1;
        }
    }
}

CNakladak& CLev::Naloz(CNakladak& _nakl)
{
    cout << "Nakladam lva na nakladak" << endl;
    return _nakl;
}

    Pro objekt CZirafa je to analogické. Funkce main() zůstává stejná jako v minulém případu.

    Zdrojový kód naleznete v sekci Downloads (projekt PrevozZOO2).

    V druhém příkladu máme operátor << třídy CNakladak přetížen jen jednou. Tento operátor zavolá díky virtuálním funkcím správnou metodu podle pravého operandu tohoto operátoru. Když nyní budeme chtít přidat další zvíře, stačí pouze přepsat metodu Naloz() a nemusíme se již vracet k hotovému objektu CNakladak. To někdy totiž ani nemusí být možné - např. nějaká knihovna.

16.2. Šablony

16.2.1. Příklady

    Nejprve si ukážeme jednoduchou šablonovou funkci a jako další příklad si ukážeme příklad s třídami. Nejčastěji uváděným příkladem je funkce pro určení minima ze dvou objektů. Napišme následující funkci:

int min(int a, int b)
{
    if(a < b) { return a; }

    return b;
}

    Jen poznamenejme, že tuto funkci lze zapsat pohodlněji pomocí ternárního operátoru:

int min(int a, int b)
{
    return a < b ? a : b;
}

    Naším dalším požadavkem bude, aby tato funkce fungovala i pro jiné typy (např. reálná čísla (double), znaky). Jedním řešením je přetěžování funkcí, kdy bychom definovali funkce se stejným jménem a různými parametry. Mnohem elegantnější je však využití šablon:

template <class T> T min(T a, T b)
{
    return a < b ? a : b;
}

    Nové klíčové slovo template znamená, že se jedná o šablonovou funkci. V závorkách je pak uveden typový parametr T. Klíčové slovo class znamená, že skutečným parametrem šablonové funkce může být libovolný datový typ (objektový i neobjektový). Ve funkci main() pak můžeme šablonovou funkci min() použít:

int main(int argc, char* argv[])
{
    int a = -5, b = -8;
    double f = 5.31f, g = 5.32f;

    cout << min(a, b) << endl;
    cout << min(f, g) << endl;

    char
d;
    cin >>
d;

    return 0;
}

    Jak tohle "kouzlo" funguje? Jednoduše, překladač nejprve zjistí, že takovéto funkce nezná, ale vidí, že zná šablonu pro funkci min(). Dosadí tedy do šablonové funkce za typový parametr T v prvním případě int a ve druhém pak double. Vzniknou tak instance funkcí int min(int, int) a double min(double, double), které pak překladač přeloží. Nyní můžeme použít libovolný typ a dosadit ho jako parametr funkce min(). Libovolným typem může být, kromě vestavěných datových typů, i instance nějakého námi definovaného objektového typu, který má přetížen příslušný operátor (v našem případě operátorovou funkci operator<()).

    Musíme si ale dát pozor na následující, nefungující kód:

int a = -5;
char c = 'a';

cout << min(a, c) << endl;

    Překladač se totiž nemůže rozhodnout, kterým typem by měl nahradit typový parametr T. Můžeme mu samozřejmě pomoci přetypováním jednoho z parametrů funkce na typ, pomocí kterého chceme provést porovnání, tedy např.:

cout << min(a, (int)c) << endl;

    Napovědět můžeme i připojením typu v závorkách, jak je vidět v následující ukázce:

cout << min<int>(a, c) << endl;

    Pokud si vzpomeneme na makra, kde by se minimum dalo implementovat takto:

#define MIN(a, b) (((a) < (b)) ? (a) : (b))

    Pak můžeme na šablony pohlížet jako na lepší makra. Šablony jsou zpracovávány překladačem, kdežto makra preprocesorem.

    Nyní si ukážeme třídu, která implementuje tzv. lineární spojový seznam, což je velmi často používaná datová struktura. Uzlem spojového seznamu nazveme strukturu obsahující nějaké datové prvky (v našem případě jednoduchou proměnnou typu int) a dále ukazatel na další uzel spojového seznamu. Lineární spojový seznam je pak řetězem takovýchto prvků. Zde se budeme zabývat jen jednoduchou implementací spojového seznamu, ale protože každý program pracuje s daty, vyjde v blízké budoucnosti článek věnovaný právě datovým strukturám. V něm se budeme zabývat například zásobníkem, frontami a také stromy. Ale teď už zpět k implementaci, která bude sestávat ze dvou tříd. První třídou bude CUzel, což bude uzel spojového seznamu. Druhou třídou bude třída CSeznam, která bude zajišťovat operace jako třeba přidání prvku, vypuštění prvku a vyhledání prvku. Nejprve si ukážeme kód pro třídu CUzel a řekneme si něco o jejím použití:

Uzel.h:

// Uzel spojoveho seznamu

template<class T> class CUzel {
private:
    T m_Data;

    CUzel* m_lpDalsi;
public:
    CUzel(T _data) : m_Data(_data), m_lpDalsi(NULL) { ; }

    void NastavData(T _data) { m_Data = _data; }
// ulozi data
    void NastavNasl(CUzel* _dalsi) { m_lpDalsi = _dalsi; }
// ulozi ukazatel na dalsi prvek   
   
    T Data() { return m_Data; }       
// vrati data
    CUzel* Nasl() { return m_lpDalsi; }       
// vrati nasledovnika

    void Vypis() { cout << "Vypis :" << m_Data << endl; }
};

    Vytvořili jsme šablonovou třídu, která je vzorem pro vytváření instancí. Tyto instance vzniknou, podobně jako u funkcí, dosazením nějakého skutečného parametru za typový parametr T. Následuje soubor main.cpp ukazující, jak s šablonou pracovat:

#include <stdio.h>
#include <iostream.h>

#include "Uzel.h"

int main(int argc, char* argv[])
{
    CUzel<int> m_intUzel(4);
    CUzel<double> m_dblUzel(15.32f);
    CUzel m_obecnyUzel();
// Nekde se prelozi, ale potom prvni pristup znamena chybu

    cout << m_intUzel.Data() << " " << m_dblUzel.Data() << endl;

    char c;
    cin >> c;

    return 0;
}

    V programu nesmí nikde být vytvořena proměnná typu CUzel, vždy to musí být CUzel a v lomených závorkách pak musí následovat skutečný parametr. Některé překladače se sice zápisu CUzel m_obecnyUzel() nebrání, takže se program v pořádku přeloží. V tom případě vygeneruje překladač hned při prvním použití této proměnné chybu.

    Vraťme se ale k naší třídě pro práci s lineárním spojovým seznamem. Tato třída bude obsahovat jeden ukazatel na třídu CUzel. Tento prvek se většinou nazývá hlavou seznamu, v našem případě nebude hlava obsahovat data (v jejím datovém prvku bychom mohli uchovávat například počet prvků v seznamu) a její ukazatel na další prvek bude ukazovat na první datový prvek obsahující data. Pro tento článek si implementujeme pouze vytvoření seznamu, vložení prvku na konec seznamu a samozřejmě uvolnění seznamu:

Seznam.h:

// Linearni spojovy seznam

template<class T> class CSeznam {
private:
  CUzel<T> *m_lpHlava;

public:
  CSeznam() : m_lpHlava(NULL)
  {
      m_lpHlava = new CUzel<T>(0);
// Vytvorime prvni prvek
  }

  ~CSeznam()
  {
      Uvolni();
// Jen uvolnime vsechny prvky
  }

  bool JePrazdny() { return (m_lpHlava == NULL); }

  void VlozKonec(T _data)
  {
     
// Overime platnost seznamu
      if(m_lpHlava)
      {
          // Vytvorime vkladany uzel
          CUzel<T> *lpVkladany = new CUzel<T>(_data);

          CUzel<T> *lpKonec;
// Najdeme si konec zretezeneho seznamu
          lpKonec = m_lpHlava;

          // Dokud je dalsi prvek platnym prvkem
          while(lpKonec->Nasl() != NULL)
          {
              lpKonec = lpKonec->Nasl();
// Posun na dalsi prvek
          }
          cout << "Vkladam za : " << lpKonec->Data() << "(" << _data << ")" << endl;

          lpKonec->NastavNasl(lpVkladany);
      }
  }

  void Uvolni()
  {
      CUzel<T> *lpMazany = m_lpHlava;

      while(m_lpHlava)
      {
          m_lpHlava = m_lpHlava->Nasl();
          cout << "Mazu : " << lpMazany->Data() << endl;
          delete lpMazany;
          lpMazany = m_lpHlava;
      }
  }

  void Vypis()
  {
     
// Nechceme vypisovat data v hlave ...
      CUzel<T> *lpVypisovany = m_lpHlava->Nasl();

      while(lpVypisovany)
      {
          lpVypisovany->Vypis();
          lpVypisovany = lpVypisovany->Nasl();
      }
  }
};

    Následuje hlavní program této šablony:

#include <stdio.h>
#include <iostream.h>

#include "Uzel.h"
#include "Seznam.h"

int main(int argc, char* argv[])
{
    CSeznam<int> m_intSeznam;

    m_intSeznam.VlozKonec(5);
    m_intSeznam.VlozKonec(8);
    m_intSeznam.VlozKonec(-5);

    m_intSeznam.Vypis();

    char c;
    cin >> c;
    return 0;
}

   Zdrojový kód naleznete v sekci Downloads (projekt Seznam).

    Program vytvoří lineární spojový seznam, kde datovými prvky jednotlivých uzlů jsou proměnné typu int. Potom vložíme do seznamu pár čísel a vypíšeme je. Samozřejmě bychom mohli do seznamu ukládat instance třídy CZivocich, kterou jsme vytvořili v minulém dílu.

    V tomto případě jsme celý seznam implementovali v hlavičkovém souboru. A tady je třeba dát pozor, šablony totiž vyžadují speciální postup při překladu, protože překladač potřebuje znát kromě deklarace šablony i těla všech metod. Pokud bychom chtěli oddělit v hlavičkovém souboru deklaraci šablony a těla metod, museli bychom pro každou funkci vytvořit šablonu. Předvedeme si to na metodě CSeznam::Uvolni(). U ostatních metod je to analogické a zdrojový kód naleznete v sekci Downloads (projekt Seznam1):

Seznam.h:

// Linearni spojovy seznam

template<class T> class CSeznam {
private:
    CUzel<T> *m_lpHlava;

public:
    CSeznam();
    ~CSeznam();

    bool JePrazdny();
    void VlozKonec(T _data);
    void Uvolni();

    void Vypis();
};

template<class T> void CSeznam<T>::Uvolni()
{
    CUzel<T> *lpMazany = m_lpHlava;

    while(m_lpHlava)
    {
        m_lpHlava = m_lpHlava->Nasl();
        cout << "Mazu : " << lpMazany->Data() << endl;
        delete lpMazany;
        lpMazany = m_lpHlava;
    }
};    // Tady je opravdu strednik

    Pokud bychom chtěli klasické rozdělení na hlavičkový a zdrojový soubor, nabízí některé překladače klíčové slovo export, které se ve zdrojovém souboru vkládá před každou šablonu metody. Některé překladače však toto slovo nemají, některé ho neumožňují spojit s klíčovým slovem template. Většinou se šablona prostě celá vloží do hlavičkového souboru a ten se pak direktivou #include vkládá do ostatních souborů.

16.2.2. Parametry deklarace šablony

    Viděli jsme dva příklady na šablony. Nyní si upřesníme, co ještě můžeme v deklaraci šablony přidat za parametry. Deklaraci šablony provádíme následujícím zápisem:

template<parametry> deklarace;

    Parametry mohou být

1)    hodnotové - jako u deklarace funkce, mohou mít implicitní hodnotu
2)    typové - pokud nedeklarujeme šablonovou funkci, pak může mít implicitní parametr

    Jako třetí druh parametru jsou uváděny šablonové parametry, se kterými se setkáme jen v nejnovějších překladačích.

    V našich příkladech jsme se setkali jen s typovými parametry. Podívejme se tedy nejprve na ně. Specifikujeme je pomocí klíčových slov class nebo typename, přičemž typename znají jen novější překladače. Za klíčovým slovem pak následuje identifikátor, který pak použijeme všude tam, kde chceme v šabloně označit typ. Zápis implicitního parametru se provede následovně:

template<class T = int> class CSeznam {
   
// Stejne jako predtim
};

    V programu pak pro využití implicitního parametru musíme pro vytvoření proměnné napsat:

CSeznam<> m_intSeznam;

    Hodnotové parametry deklarujeme jako parametry funkcí. Následuje ukázka deklarace:

template <class T, unsigned int hodnotovy_parametr /*= impl_hodnota*/> class Sablona { telo_sablony; };

    Pomocí hodnotových parametrů lze výhodně implementovat např. vektory, kde prvkem bude ukazatel, který bude ukazovat na dynamicky alokované pole prvků typu T. Jen matematická poznámka pro ty, kteří ještě neměli tu čest setkat se s vektory, tak vektor je n-tice čísel. Může určovat např. souřadnice v 2D nebo 3D prostoru. Ještě zdůrazňuji, že hodnotové parametry mohou mít implicitní hodnotu.

16.3. Co bude příště?

    Příště doplníme ještě pár věcí k šablonám. Jako další téma probereme výjimky a v některém z dalších dílů se seznámíme se standardní šablonovou knihovnou, která je součástí C++. Naší pozornosti ale neuniknou ani objektové vstupně-výstupní proudy.

Příště nashledanou.

Ondřej Burišin