Šablony funkcí

C a C++ C++ Objektově orientované programování Šablony funkcí

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

Šablony jsou velice mocným nástrojem jazyka C++. Můžeme mít jak šablony samostatných funkcí či členských funkcí tříd, tak šablony celých tříd. V tomto tutoriálu se zaměřím na šablony funkcí. Šablona funkce je pouze jakýsi návod s několika proměnnými faktory, podle kterého kompilátor vytvoří výslednou funkci ve chvíli, kdy ji poprvé použijeme. Šablona funkce se tedy sama vůbec nekompiluje, a proto definice šablonových funkcí píšeme do hlavičkových souborů (.h nebo .hpp) a ne do zdrojových souborů (.cpp). Šablonu deklarujeme klíčovým slovem template, po něm následují parametry šablony v ostrých závorkách, posléze návratový typ funkce, název funkce a parametry funkce v kulatých závorkách. Stejně jako u klasických funkcí, můžeme ukončit příkaz středníkem a šablonu definovat jinde, zde ale v názorných ukázkách budu šablony definovat rovnou při deklaraci.

template <parametry_šablony>
návratový_typ název_funkce(parametry_funkce)
{
  tělo_funkce
}

Šablona může mít tři typy parametrů. Nejčastěji používaný parametr šablon je typename/class (v daném kontextu je typename i class úplně to samé, já budu používat typename). Tento parametr vytvoří proměnný datový typ, za který při použití funkce můžeme dosadit jakýkoliv typ, který je funkce schopná zpracovat na základě použitých operátorů a konverzí. Představme si funkci, která má za úkol prohodit hodnoty dvou proměnných či objektů, přičemž chceme, aby tato funkce byla použitelná pro jakýkoliv datový typ. Není nic jednoduššího, než vytvořit šablonu.

template <typename type>  //vytvoření šablony s parametrem typename type
void SwapFunction(type & a, type & b)  //šablonová funkce přebírá dvě reference typu type, datový typ, který se použije místo slova type, určíme při volání funkce
{
  type c = a;  //deklarace proměnné typu type
  a = b;
  b = c;
}
...
int num1 = 10;
int num2 = -5;
double val1 = 0.123;
double val2 = 14.02;
SwapFunction(num1,num2);  //prohození hodnot typu int
SwapFunction(val1,val2);  //prohození hodnot typu double

class trida {...};
...
trida obj1 = ...;
trida obj2 = ...;
SwapFunction(obj1,obj2);  //prohození hodnot objektů třídy, každá třída má implicitně implementován operátor =, takže toto je možné

Kompilátor podle předaných parametrů při volání funkce pozná, jaký datový typ má dosadit za type. Vytvoří a následně zavolá úplně novou funkci, která například v případě předání proměnných typu int, vypadá takto:

void SwapFunction(int & a, int & b){int c = a; a = b; b = c;}

Teprve tato funkce je kompilována a má už i svojí adresu v paměti. Pokud bychom chtěli vytvořit pointer na šablonovou funkci, musíme určit, kterou funkci, vytvořenou podle dané šablony, máme na mysli. Nemůžeme prostě říci kompilátoru, že chceme získat adresu funkce SwapFunction, taková funkce vůbec neexistuje. Můžeme ale říci, že chceme adresu funkce vytvořené podle šablony SwapFunction s parametrem například char. Tedy SwapFunction<char>.

void (*FunctionPtr)(char &, char &) = SwapFunction<char>;  //pokud  jsme funkci SwapFunction<char> doteď nepoužili, kompilátor jí v tuto chvíli vytvoří

Tento zápis můžeme použít i pro volání funkce. Pokud bychom například měli šablonu, u které podle předaných parametrů není možné rozpoznat datový typ, který chceme dosadit za parametr šablony, nemáme jinou možnost.

template <typename type>
type GetRandomNumber(int seed)
{
  type value;
  ...  //aloritmus pro vygenerování náhodného čísla
  return value;
}
...
int rndValue = GetRandomNumber(20);  //toto nebude fungovat, funkce GetRandomNumber neexistuje
int rndValue = GetRandomNumber<int>(20);  //toto fungovat bude, kompilátor vytvoří funkci podle šablony GetRandomNumber které předá parametr int
                                          //tedy vytvoří funkci GetRandomNumber<int> a zavolá ji
double rndValue = GetRandomNumber<double>(20);  //to samé, pouze pro typ double

class Auto {...};
...
Auto fiat = GetRandomNumber<Auto>(20);  //toto fungovat nebude, leda že by třída auto měla implementovány všechny potřebné operátory a konverze pro provedení algoritmu uvnitř funkce

Šablona samozřejmě může parametrů přebírat více.

template <typename type1, typename type2>
void function(type1 a, type2 b)
{
  ...  //nějaký kód
}
...
int a = 0;
double b = 1.45;
function(a,b)  //v tomto případě kompilátor sám pozná co za parametry dosadit

template <typename type1, typename type2> void function(){}
...
function<int,double>();  //v tomto případě to musíme kompilátoru říci my

Další typ parametru, který může šablona přebírat, je celočíselná konstanta. Můžeme tedy šabloně předat konstantní hodnotu jakéhokoliv celočíselného datového typu, například int, char, long int, short int, ale i int*, char* či double*, pointer je totiž také celočíselný datový typ. Nemůžeme ale předat například hodnotu typu double nebo float. Řekněme, že chceme mít funkci, která ve svém těle deklaruje pole nějakého datového typu o určité velikosti. Toto pole nechceme vytvořit dynamicky, ale velikost statického pole bychom vždy měli určit konstantou. Neměli bychom tedy velikost pole předat parametrem funkce. Můžeme ale vytvořit šablonu, která nám pro každou velikost tohoto pole vytvoří jinou funkci. Velikost pole určíme parametrem šablony.

template <typename type, int n>  //vytvoření šablony s parametry typename type a int n
void function()
{
  ...
  type array[n];  //deklarace našeho pole
  ...  //nezáleží na tom co funkce s polem dělá
}
...
function<int,10>();  //vytvoření funkce s parametry int a 10 podle šablony a zavolání této funkce
function<int,30>();  //to samé, pouze za n dosadíme 30
function<double,40>();  //to samé, pouze za type dosadíme double a za n 40
function<int,10>();  //zde už kompilátor funkci nevytváří, jelikož už existuje, pouze ji zavolá

Musíme pořád brát v úvahu, že například function<int,10> a function<int,30> nejsou jedna a ta samá funkce, jsou to pouze dvě různé funkce vytvořené podle stejné šablony a každá z nich má jinou adresu v paměti, a to i přesto, že do obou dosazujeme stejný datový typ za type. I těchto parametrů může šablona samozřejmě přebírat více a je jedno v jakém pořadí parametry budou. Můžeme mít klidně takovouto šablonu:

template <typename type1, int n, char* ptr, typename type2, short int num, typename type3> ...

Třetím typem parametru šablony je šablona třídy. Ano, šablona může jako parametr přebírat další šablonu. Ovšem o šablonách tříd tu zatím byla pouze drobná zmínka a nejsou předmětem tohoto tutoriálu, proto tento třetí typ parametru zatím necháme být.

Specifikace šablon

Specifikace šablony říká kompilátoru, že pro určitou konkrétní signaturu má funkci vytvořit jinak, než podle základní šablony. Toto není zase až tak podstatná věc, protože tohoto chování můžeme stejně tak docílit tím, že vedle šablony vytvoříme jednu obyčejnou funkci, se stejným názvem a tou danou signaturou, pro kterou chceme dané chování funkce definovat. Kompilátor dá vždy přednost klasické funkci před šablonovou.

int function(int a){...}  //funkce č.1
template <typename type> type function(type a){...}  //šablona funkce č.2
...
int x = 5;
double y = 45.78;
function(y);  //vytvoří funkci podle šablony funkce č.2 a zavolá ji
function(x);  //zavolá funkci č.1, klasická funkce má větší prioritu než funkce šablonová

Stejného efektu můžeme dosáhnout pomocí specifikace šablony. Ta se vytváří následovně:

template <typename type> type function(type a){...}  //klasická šablona
template <> int function<int>(int a){...}  //specifikace šablony pro typ int
...
int x = 5;
double y = 45.78;
function(y);  //vytvoří funkci podle šablony a zavolá ji
function(x);  //vytvoří funkci podle dané specifikace šablony a zavolá ji

Dobré je také vědět, že klasická funkce má vždy přednost i před jakoukoliv specifikací šablony.

template <typename type> type function(type a){...}  //šablona funkce č.1
template <> int function<int>(int a){...}  //specifikace šablony pro funkci č.2
int function(int a){...}  //funkce č.3
...
int x = 5;
double y = 45.78;
function(y);  //vytvoří funkci podle šablony funkce č.1 a zavolá ji
function(x);  //zavolá funkci č.3
...

Funkce č.2 se nikdy nezavolá, leda že bychom si ji vyžádali explicitně.

...
function<int>(x);  //explicitní volání funkce vytvořené podle specifikace šablony funkce č.2

Šablona členské funkce třídy

Můžeme mít samozřejmě i šablonu členské funkce třídy. Vytvoříme jí jednoduše.

class trida
{
  template <typename type, int n> void function(type);  //deklarace šablony funkce v bloku třídy
};

template<typename type, int n>
void trida::function(type val)  //definice šablony funkce
{
  ...  //nějaký kód
}

Platí pro ní stejná pravidla jako pro šablonu samostatné funkce.


 

 

Článek pro vás napsal Lukáš Hruda (Luckin)
Avatar
Jak se ti líbí článek?
4 hlasů
...
Aktivity (1)

 

 

Komentáře
Zobrazit starší komentáře (2)

Avatar
Lukáš Hruda (Luckin):3.4.2013 16:22

Otázka je, jestli se z toho někdo něco naučí :D

 
Odpovědět 3.4.2013 16:22
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Drahomír Hanák:3.4.2013 16:25

Když teda udělám template třídy, jak ji pak můžu používat? To kompilátor taky vytvoří novou třídu?

Příklad: Mám definovanou třídu Stack a k ní template. V kódu používám třeba Stack<int> a Stack<double> Jsou to tedy 2 rozdílné třídy? Můžu se nějak odkázat na Stack nebo musím explicitně uvést datový typ? To by totiž bylo celkem omezující.

 
Odpovědět 3.4.2013 16:25
Avatar
Odpovídá na Drahomír Hanák
Lukáš Hruda (Luckin):3.4.2013 16:46

Jsou to dvě rozdílné třídy. Není ale problém pomocí šablonové členské konverzní funkce definovat konverzi mezi Stack<type1> a Stack<type2>, kde type1 a type2 jsou rozdílné typy. Popřípadě pomocí přetíženého operátoru, záleží jak to přesně potřebuješ a jestli máš možnost tu třídu změnit.

 
Odpovědět 3.4.2013 16:46
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Odpovídá na Lukáš Hruda (Luckin)
Drahomír Hanák:3.4.2013 17:05

Aha. Nejde mi ani tak o konverzi jako spíš o mnou definovaný typ. Co kdybych měl další třídu, která by pracovala s třídou Stack (obecně s kterýmkoli datovým typem - Stack<int>, Stack<double>, Stack<whatever>)? Jestli to chápu dobře, musel bych k tomu ještě definovat společného předka, abych ji mohl předat třeba v konstruktoru:

class IStack { ... };

template <class T>
class Stack : IStack {
   public:
      add(T element);
};

class Manipulator {
   public:
      Manipulator(IStack *stack);
};

Což se mi moc nelíbí. Nejde se tedy nějak odkázat na Stack, aby mi konstruktor třídy Manipulator vzal všechny možné třídy vytvořené z šablony Stack? V podstatě aby to fungovalo trošku jako genericita :P

Editováno 3.4.2013 17:07
 
Odpovědět 3.4.2013 17:05
Avatar
Odpovídá na Drahomír Hanák
Lukáš Hruda (Luckin):3.4.2013 17:11

Pokud jsi to myslel tak, že chceš udělat pointer nebo referenci, přes kterou půjde přistupovat k objektu jakékoliv třídy vytvořené podle šablony Stack, tak to nejde, jedině bys musel při každém použití ten odkaz přetypovat, na to ti ale stačí void*. Pokud chceš jenom int a double, pak to můžeš udělat přes nějakou abstraktní základní třídu, která bude mít všechny metody deklarované virtuálně, a to jak pro int, tak pro double (např. budeš muset udělat virtual void push(int) i virtual void push(double)), pak šablonu Stack odvodíš od téhle třídy.

 
Odpovědět 3.4.2013 17:11
Avatar
Odpovídá na Drahomír Hanák
Lukáš Hruda (Luckin):3.4.2013 17:19

Musel bys Manipulator udělat taky jako šablonu. Pak bys ale třeba pro Stack<int> měl Manipulator<int> a pro Stack<double> Manipulator<dou­ble>.

 
Odpovědět 3.4.2013 17:19
Avatar
Odpovídá na Lukáš Hruda (Luckin)
Lukáš Hruda (Luckin):3.4.2013 17:22

Nebo pomocí šablonových členských funkcí.

class Manipulator {
   public:
      template <typename type> Manipulator(Stack<type> *stack);
};

Záleží co ta třída ve výsledku s těma datama dělá.

 
Odpovědět 3.4.2013 17:22
Avatar
Drahomír Hanák
Tým ITnetwork
Avatar
Odpovídá na Lukáš Hruda (Luckin)
Drahomír Hanák:3.4.2013 17:35

Ok, díky za komentáře. Na Manipulator bych nepřenášel ten typ. To by bylo celkem nepraktické. Ten poslední způsob se mi zdá nejlepší.

 
Odpovědět 3.4.2013 17:35
Avatar
Odpovídá na Drahomír Hanák
Lukáš Hruda (Luckin):3.4.2013 17:47

Pomocí šablon metod, pointerů na metody a void* pointerů můžeš v rámci té třídy s těmi daty dělat cokoliv bez explicitního určení typu z venku. Jediný problém je vrácení Stacku metodou, tam musíš vždycky explicitně přetypovat. Ta třída si nemůže nijak vést záznam o tom, jaký typ má vrátit. Jinak to jde asi vždycky

 
Odpovědět 3.4.2013 17:47
Avatar
jakub.pecenka:25. dubna 21:47

Super článek, díky!

 
Odpovědět 25. dubna 21:47
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 12. Zobrazit vše