V minulém dílu jsme načali šablony, dnes budeme pokračovat a probereme si tzv. specializaci šablon. Potom bude následovat odstaveček věnovaný chybám, seznámíme se s mechanizmem výjimek jazyka C++.
Pokud chceme uvnitř šablony třídy mít statický atribut, např. pro počítání vytvořených instancí, musíme - podobně jako při definici metody - použít plného zápisu:
template<class T> class Test {
private:
static int pocet;
public:
// Metody
Test() { pocet++; }
~Test() { pocet--; }
static VypisPocet() { cout << pocet << endl; }
};
template<class T> int Test<T>::pocet = 0;
Lze také přiřadit šabloně založené na nějakém typu jinou implicitní hodnotu. Pokud chceme aby např. všechny šablony, které budou mít typový parametr int, začínaly se statickou proměnnou pocet na hodnotě 5 můžeme napsat:
int Test<int>::pocet = 5;
Ukážeme si příklad funkce main():
int main(int argc, char* argv[])
{
Test<int> mujtest;
Test<float> mujtest2;
Test<int>::VypisPocet();
// vypise 6 - pocet trid test s hodnotami
int
Test<float>::VypisPocet();
// vypise 1 - pocet trid test s hodnotami
float
char c;
cin >> c;
return 0;
}
Pokud potřebujeme šablonu, která by měla pracovat s jedním nebo více různými typy šablonového parametru jinak, pomůže nám právě specializace. Lze tedy vytvořit základní šablonu a pak například pro typ char* upravit některé metody. To se může hodit, jestliže chceme vytvořit funkci pro porovnávání dvou typů. U čísel máme jednoznačně určené uspořádání, ale pro řetězce bychom mohli specializovat tuto šablonovou funkci tak, aby vracela slova v pořadí, v jakém jsou uvedena ve slovníku. Ukážeme si jiný příklad, budeme specializovat třídy, ale to samé lze provést i pro šablonové funkce:
#include <iostream.h>
#include <string.h>
template<class T> class Container {
private:
T m_pole[20];
public:
Container();
VlozDoPole(int _index, T _prvek);
T VyberZPole(int _index);
};
template<class T> Container<T>::Container()
{
for(int i = 0; i < 20; i++)
{
m_pole[i] = 0;
// inicializace
}
};
template<class T> Container<T>::VlozDoPole(int _index, T _prvek)
{
if(_index < 20) // Jsme-li v poli
{
m_pole[_index] = _prvek;
}
};
template<class T> T Container<T>::VyberZPole(int _index)
{
if(_index < 20)
{
return m_pole[_index];
}
else
{
return 0;
}
};
Pokud budeme chtít použít třídu Container k uchování proměnných typu int bude vše v pořádku. Co se ale stane, pokud se rozhodneme ukládat řetězce následujícím způsobem:
Container<char *> stringCont;
char* retezec = new char[5];
// jeden znak na '\0'
strcpy(retezec, "TEST");
stringCont.VlozDoPole(0, retezec);
Metoda stringCont::VlozDoPole() přiřadí do pole ukazatel na řetězec "TEST", problém je v tom, že pokud nyní změníme hodnotu proměnné retezec, nebo ji dokonce pomocí delete smažeme, tak se změny promítnou i do naší instance třídy Container. To samozřejmě nechceme, v prvním případu by došlo k přepsání uložených dat. Ve druhém by v nejhorším případu mohlo dojít k přístupu k paměti, která není naše. Pomůžeme si tedy například specializací šablony Container pro typ char*:
template<> class Container<char*> {
private:
char* m_pole[20];
public:
Container();
~Container();
VlozDoPole(int _index, char* _prvek);
char* VyberZPole(int _index);
};
template<typename> Container<char*>::Container()
{
for(int i = 0; i < 20; i++)
{
m_pole[i] = NULL;
}
}
template<> Container<char*>::~Container()
{
for(int i = 0; i < 20; i++)
{
delete m_pole[i];
// uvolnime pamet
}
}
template<> Container<char*>::VlozDoPole(int _index, char* _prvek)
{
if(_index < 20)
{
int ilen = strlen(_prvek) + 1;
// znak '\0'
m_pole[_index] = new char[ilen];
if(m_pole[_index])
{
strcpy(m_pole[_index], _prvek);
}
}
}
template<> char* Container<char*>::VyberZPole(int _index)
{
if(_index < 20)
{
return m_pole[_index];
}
else
{
return NULL;
}
}
Vidíme, že specializace se provede pomocí zápisu:
template<> class Container<typ> {
... }
// v deklaraci tridy
template<> navrat_typ JmenoTridy<typ>::JmenoMetody(parametry)
{ }
// u metod
Pokud by nás napadlo, že bychom provedli specializaci pro ukazatele na T, museli bychom napsat následující:
template<class T> class Container<T*> {
... }
// v deklaraci tridy
template
Bohužel takto definovaná šablona se v překladači Microsoft Visual C++ 6.0 nepřeloží, nepřeloží se ani v nejnovějším produktu firmy Microsoft - Visual Studiu .NET. Ale například pod Borland C++ Builderem verze 4.0 to přeložit jde, ukážeme si jen jak by třída byla definována a jednu metodu:
template<class T> class Container<T*> {
private:
T* m_pole[20];
public:
Container();
~Container();
VlozDoPole(int _index, T* _prvek);
T* VyberZPole(int _index);
};
template<class T> Container<T*>::Container()
{
for(int i = 0; i < 20; i++)
{
m_pole[i] = NULL;
}
}
V některých starších překladačích se můžeme setkat se zápisem specializace následujícím způsobem:
class JmenoTridy<int*> { }
Předpokládejme, že Container je objektová šablona z předchozího odstavce, doposud jsme instance šablony vytvářeli následujícím způsobem:
Container<int> intCont;
Jazyk C++ nám ale nabízí ještě jinou možnost, explicitní vytvoření instance šablony. Provede se to takto:
template class Container<int>;
Když použijeme v programu šablonu
vytvořenou prvním způsobem, překladač pro ni nemusí vytvořit úplně všechny
metody. Překladač jazyka C++ totiž přeloží jen ty funkce které opravdu v
programu zavoláme. Existuje pár výjimek, např. virtuální metody se vytvářejí
vždy.
Pokud vytvoříme šablonu druhým způsobem, tedy explicitně, dojde k
vytvoření všech metod.
Instance šablony jsou rovnocenné s klasicky vytvořenými třídami, lze je tedy použít jako předky odvozené třídy. Zdrojový kód výše uvedeného příkladu naleznete v sekci Downloads (projekt Specialiazace).
Chvilku se zamyslete, jak by byl svět krásný, kdyby chyby prostě neexistovaly. Uživatelé by vždy zadali na vstupu správné údaje, spojení dvou počítačů přes síť by se nikdy nepřerušilo a hlavně: programy by neobsahovaly ani jednu chybu. No a teď hurá zpět do reality, kde každý, i sebelépe napsaný, program obsahuje hromady chyb. Není se čemu divit, užitečné programy jsou většinou pěkně složité. Nejprve si povíme, jaké máme možnosti detekce chyb a pak se podíváme na jazykem C++ nabízenou obsluhu výjimek.
Nejprve je nutné rozlišit na chyby, které můžeme rovnou ošetřit, protože známe dostatek informací o vzniklém problému. Ale existují i chyby, u nichž nevíme, proč přesně vznikly a je třeba poslat zprávu o chybě do širšího kontextu (např. volající funkci), kde už jsou potřebné informace k dispozici. Prvním druhem chyb se nebudeme zabývat, protože jdou jednoduše ošetřit. Vrhneme se proto na ten zajímavější druh. V následujícím odstavečku jsou uvedeny možné reakce na chybu.
Pokud se podíváte do dokumentace překladače, určitě si všimnete, že existuje mnoho funkcí s návratovou hodnotu. Tato hodnota je většinou použita k přenosu příznaku chyby. Programátor ale bývá tvorem líným a tak málokdy píše "ty zbytečné" řádky pro ověření návratového kódu. Vzpomeňme si třeba na operátor new, který v případě chyby při alokaci paměti vrátí hodnotu NULL. Je pravdou, že při takto důležité operaci většinou návratovou hodnotu ověříme. Ale třeba u takové funkce fprintf() neověřujeme, kolik znaků bylo zapsáno do výstupního souboru, což je přesně číslo, které funkce vrací. Určitě se ale najdou funkce, které si nemůžou dovolit plýtvat hodnotami vracenými z funkce. Potom některé funkce ze standardní knihovny jazyka C nastavují globální příznak chyby (proměnná errno a funkce perror()). Další z možností je pak použít velice málo známého i používaného mechanizmu obsluhy signálů. Poslední možností je pak použít funkcí setjmp() a longjmp(). První z těchto funkcí uloží všechny důležité informace procesoru do bufferu, druhá funkce tyto informace obnoví a program tak může pokračovat z místa, kde byl skok nastaven. Je jasné, že úplně nejhorší reakcí na chybu je, když program vypíše chybovou hlášku a z ničeho nic se ukončí.
Nevýhodou výše uvedených funkcí je, že si programátor může říct: "Potřebuji rychle funkční program, kontrolu chyb dodělám jindy". Jenže je jasné, že k tomu pak už většinou nedojde. Dalším problémem je kontrola chyb, kód programu je pak strašlivou směsicí kódu, který kontroluje, jestli funkce proběhla bez problémů, a vlastního výkonného kódu. To pak značně snižuje čitelnost kódu.
Jak víme, tak v jazyce C++ můžeme vytvářet objektové typy. Ty při vzniku volají konstruktor pro svou inicializaci a při zániku by měl být zavolán destruktor, aby došlo k "úklidu". Pokud funkci opustíme pomocí signálů nebo funkce longjmp(), nedojde k zavolání destruktorů, což činí návrat z výjimečného stavu skoro nemožným. V případě, že dosáhneme výjimečné situace ve funkci, která vrací chybu přes návratovou hodnotu, nemusí mít žádný smysl dále ve funkci pokračovat.
Jinou možností zachycení chyby jsou právě výjimky. Fungují zjednodušeně tak, že v místě vzniku výjimky vytvoříme objekt, který ponese informace o vzniklém problému a tento objekt "vyhodíme" z funkce. Výjimky mají výhodu, že se kód dělí na část, která se má provést a část, kde se staráme o vzniklé chyby. Druhou výhodou je, že programátor musí chyby ošetřit. Pokud funkce totiž vyhodí výjimku a volající funkce ji nezachytí, tak se výjimka šíří do nadřazených bloků, dokud ji někdo nezachytí.
V programu můžeme vyvolat tzv. synchronní výjimku pomocí klíčového slova throw. Jak jsme si uvedli, můžeme z funkce vyhodit libovolný objekt. Tímto objektem může být jedna ze standardních výjimek, řetězec (tedy ukazatel na char) nebo naše třída. V následujícím kódu funkce vyvolá výjimku, pokud bude jako parametr zadáno záporné číslo:
int Test(int a)
{
int iResult = 0;
if(a < 0)
{
throw "Cislo je mensi nez 0";
}
// dalsi vypocet
return
iResult;
}
Pokud tuto funkci zavoláme se záporným parametrem ve funkci main() dojde k vyvolání výjimky a program se ukončí, protože výjimku nikdo nezachytil. Napišme nyní obsluhu výjimky. To provedeme tak, že kód který chceme provést (tedy funkci Test()) umístíme do pokusného bloku try a za blok try umístíme blok catch, který zachytí výjimku. Kód bude vypadat následovně:
int main() {
try {
int iResult = Test(-5);
// dalsi kod pracujici s vysledkem
}
catch(char* e)
{
cout << e << endl;
}
char c; //
cekame na stisk klavesy
cin >> c;
return 0;
}
To je nejjednodušší reakce na chybu. Klíčové slovo throw způsobí vytvoření objektu, přičemž se pro něj zavolá konstruktor. Potom je tento objekt vrácen z funkce, ačkoliv funkce Test() vrací hodnotu s typem int. Kód popsaný komentářem se neprovede, protože by ani nemělo smysl ho provést se špatným výsledkem funkce Test(). Navíc se zruší všechny objekty, které byly v době vzniku výjimky správně vytvořeny. Pokud mají instance objektových typů paměťovou třídu auto, jsou zavolány jejich destruktory. Pokud bychom se z funkce vraceli normálním způsobem, byly by zrušeny veškeré objekty.
Potom následuje zachycení výjimky v bloku catch - tzv. handler (obsluha výjimky). V závorkách za klíčovým slovem je uveden typ výjimky, který se má zachytit. Pokud je to stejný typ výjimky, který byl vyvolán, vstoupí program do tohoto bloku. V bloku je možné provést úklidové práce - například uzavření souborů, pokud bychom čísla četli z pevného disku. Pokud se nebude typ výjimky shodovat dojde k šíření výjimky do nadřazeného bloku, tedy do funkce main(), v tomto případě by opět došlo k ukončení programu, protože by vznikla nezachycená/neobsloužená výjimka (unhandled exception). V případě vzniku nezachycené/neobsloužené výjimky dojde k zavolání funkce terminate(). Tato funkce nevolá destruktory objektů a pokud dojde k zavolání této funkce, je to jen a jen chyba programátora. V případě potřeby lze tuto funkci předefinovat na jinou zavoláním funkce set_terminate(). Tato funkce vrací ukazatel na původní funkci terminate(), jejíž adresu je vhodné uschovat a obnovit. Na námi definovanou funkci pro ukončení jsou kladeny následující požadavky:
nesmí vracet žádnou hodnotu (musí tedy být void)
nemá žádné parametry
nesmí vyvolat výjimku
musí zavolat funkci, která ukončí program (zavolání terminate() znamená, že program se nemůže z chyby vrátit)
Následuje příklad pro nastavení vlastní ukončovací funkce:
#include <iostream.h>
#include <stdlib.h>
#include <eh.h>
void konecna() {
cout << "Chyba z ktere se nelze vratit!" << endl;
exit(0);
}
void (*old_terminate)() = set_terminate(konecna);
// nastaveni vlastni funkce terminate()
int main()
{
try {
throw "Chyba";
// vyvolame vyjimku
}
catch(char*)
{
// zachyti retezce
terminate(); // dojde k okamzitemu ukonceni programu
}
return 0;
}
Jen poznamenejme, že tohle je ukázkový příklad. Takhle by neměl program nikdy dopadnout.
Za jedním blokem try může následovat více handlerů, takže lze chytat více různých druhů výjimek. Můžeme mít například:
try {
//blok try
}
catch(char* e)
{
// zachyti retezce
}
catch(int e)
{
// zachyti integer
}
catch(...)
{
// zachyti vse ostatni
}
Znak ... znamená, že tímto handlerem jsou zachyceny všechny výjimky. Je jasné, že může být uveden pouze na konci seznamu handlerů, jinak by přebral práci všem ostatním. Po vykonání těla jednoho handleru program ostatní přeskakuje.
Jazyk C++ přímo nepodporuje možnost zotavení z výjimky. Jednoduše předpokládá, že handler ukončí korektně program. To ovšem nemusí být vhodné, především u real-time aplikací. Ukážeme si upravený program, kde číslo, které bude předáno jako parametr funkci Test() bude zadávat uživatel. V případě špatného čísla dojde k vyvolání výjimky:
#include <iostream.h>
int Test(int a)
{
int iResult = 0;
if(a < 0)
{
throw "Cislo je mensi nez 0";
}
// dalsi vypocet
return
iResult;
}
int main()
{
int input = 0;
int baddata;
while(baddata) {
try {
cout << "Zadejte
cislo: ";
cin >> input;
int iResult =
Test(input);
// Dalsi
prace s iResult
baddata = 0;
}
catch(char* e)
{
cout << e <<
endl;
baddata = 1;
}
}
char c;
cin >> c;
return 0;
}
Kód naleznete v sekci Downloads (projekt Except1).
V proměnné baddata máme uloženo jestli uživatel zadal chybná data. Pokud vznikne výjimka, byla zadána špatná data a smyčka pokračuje. Jestliže proběhne celý blok try, je smyčka ukončena.
Kurz jazyka C++ se nám pomalu blíží ke svému konci. Příště dokončíme výjimky, povíme si něco o prostorech jmen. Doposud jsme používali objektové proudy jen ke vstupu z klávesnice nebo výstupu na monitor, ukážeme si tedy, jak pomocí nich číst a zapisovat z/do souboru.
Příště nashledanou.