Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Dědičnost šablon, CRTP a Invoker - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Dědičnost šablon, CRTP a Invoker

OOP prvky lze používat i při generickém programování pomocí šablon. Dědit šablony je snadné, podívejme se i na polymorfické šablony.


Dědíme šablonu

Mluvíme-li o dědění šablony, myslíme tím šablony tříd. Dědit šablonu je snadné, protože vlastně dědíme třídu.

template<class A>
class ZakladniSablona {...};

template<class A>
class RozsirenaSablona: public ZakladniSablona<A> {...};

Děděním vznikne šablona, která obsahuje všechny vlastnosti základní šablony, stejně jak očekáváme u tříd.

Polymorfní šablony

Dědění šablony může mít význam, pokud rozšiřujeme existující šablony o nové funkce. Často ale potřebujeme u šablony zřídit polymofrní chování, jako je to u tříd a objektů. Tady se člověk dost často zamotá, protože je třeba si uvědomit, zda chceme polymorfismus řešit na úrovni šablon, nebo na úrovni objektů.

Pokud jde o objekty, tam se nic nemění. Metody, které mají být implementované specificky pro různé potomky označíme klíčovým slovem virtual a tím zařídíme pozdní vazbu.

Ale velice často se uchylujeme k šablonám proto, že nechceme, nebo nemůžeme použít pozdní vazbu. Naopak by jsme rádi, aby překladač provedl jakési skutečné dědění šablon (na úrovni šablon) a výsledkem byla instance šablony (třída), která již neřeší dědění a polymorfismus. Prostě jinými slovy, výstupem musí být běžná, nepolyformní třída. Pokud někde existuje polymorfizums, existuje pouze behem instanciování šablon.

Curiously recurring template pattern (CRTP)

Jakési řešení nabízí Curiously recurring template pattern (CRTP). Ten umožňuje během instanciování třídy instanciovat i předka této třídy na míru. Zapíšeme to takto:

template<class Potomek>
class ZakladniTrida {...};

template<class A>
class RozsirujiciTrida: public ZakladniTrida<RozsirujiciTrida<A> > {...};

Všimněme si, že opravdu dochází k instanciování šablony ZakladniTrida podle šablony RozsirujiciTrida.

K čemu je to vhodné? Při návrhu šablon tříd, které mají zajišťovat nějakou činnost brzo narazíme na problém "zpětného volání". Základní třída například při volání některé její metody potřebuje, aby nějakou činnost definovala třída, která ji dědí. Podle toho, která třída dědí tuto základní třídu pak můžeme ovlivnit činnost. Prostě tak jak jsme zvyklí u pozdní vazby (funkce s klíčovým slovem virtual). Jenže, pokud nepoužijeme pozdní vazbu, můžeme v každé třídě přistupovat pouze k členům této třídy, nebo ke všem členům tříd, které tato třída dědí. Rozhodně nemáme přístup k členům třídy, která tuto třídu dědí. A to je nemilé, protože se nezmohneme ani na zpětné volání.

A právě CRTP nabízí řešení. To, která třída dědí základní třídu můžeme dodat parametrem šablony, tak jak je naznačeno v příkladu nahoře. Tuto informaci pak můžeme jednoduše využít. Například takto:

template<class Potomek>
class ZakladniTrida
{
public:
void foo()
{
static_cast<Potomek *>(this)->bar();
}
};

Funkce foo je napsaná tak, aby demonstrovala, jak lze zavolat funkci bar, která je (bude) deklarovaná v potomkovi. Tuto funkci už tady můžeme zavolat, aniž bysme o ní měli nějaké povědomí. Třída, která zde vystupuje pod jménem Potomek, ji bude implementovat. Všimněte si, že zde není pozdní vazba

Důsledek CRTP

Možná si teď uvědomujete, jak OOP dostává na frak. Představme si, že takto dědíme 10 různých tříd. Každá třída tedy kromě sebe instanciuje třídu předka na míru sama sobě. V konečném důsledku máme 10 různých hierarchí a každý předek nemá jinou možnost, než být zděděn příslušným potomkem.

Obrázek ukazuje, jak vypadá hierarchie u šablony GenericArray

Není možné se takto na to koukat. Programování pomocí šablon nelze mýchat do OOP v C++. Je sice pravdou, že vzniká N předků pro N potomků, ale předkové nemají žádný význam pro jiné třídy. Můžeme tedy každou vygenerovanou hiearchii schovat do koncových tříd jako implementační detail. Výsledkem je přesně to co jsme potřebovali, nutnost pozdní vazby byla odstraněna a výstupem je prostá třída bez dědění a polymorfizmu šablon.

Podívejme se ještě na OOP zhlediska šablon. Ani tady nedochází k žádným narušením. Šablona předka je jen jedna a obsahuje všechen společný kód pro všechny potomky. Tak jak jsme u OOP zvyklí.

Rozhraní v šablonách

Umíme v šablonách pozdní vazbu nahradit pomocí CRTP, bylo by možné zavést něco jako rozhraní (interface)?

Při programování pomocí šablon můžeme programovat skutečně čistě objektově - to co v C++ nejde - beztypově. To jestli jeden objekt (objekt je instance šablony) umí komunikovat s jiným objektem není dáno typem objektu, ale prostě tím, jestli objekt, který stojí na opačné straně implementuje příslušný komunikační protokol. Přirovnat to můžeme komunikaci dvou lidí, kdy komunikačním jazykem je čeština. V C++ by to znamenalo, že takový člověk může komunikovat pouze s čechem (pokud předpokládáme, že vlastností čechů je komunikace jazykem čeština). Tady však není řečeno, kdo má stát na druhé straně, ale pouze to, že musí umět česky. Takže to klidně může být i japonec.

Představme si, že potřebujeme napsat nějakou šablonu, která sečte prvních 10 prvků v poli. Musí být univerzální, protože nevíme, jaké pole bude použito:

template<class Arr, class Vysl>
void SectiPrvnichN(const Arr pole, int pocet, Vysl &vysledek)
{
vysledek = 0;
for (int i = 0;i<pocet;pocet++) vysledek = vysledek + pole[i];
}

Funkci můžeme použít jak na float * tak na nějaký sofistikovaný kontejner, třeba vektor. Nezáleží nám, jaký typ pole bude do funkce vstupovat, postačí, když víme, že implementuje operator [].

Změní se nějak deklarace funkce, když budeme vyžadovat, aby funkce pracovala jen s vektorem? Někdy se hodí přeci jen množinu tříd, které mohou být použity v dané šabloně nějak omezit. Nebo si vynutit, aby tyto třídy patřily do nějaké společné množiny tříd.

Tuto společnou množinu tříd můžeme definovat pomocí CRTP. Rozhraní definujeme přímo v základní třídě. Toto rozhraní pak implementujeme v rozšířené třídě.

template<class Potomek, class TypPrvku>
class GenericArray
{
public:
TypPrvku operator[](int index) const {...}
};

Všechny třídy, které chtějí požívat výhod skupiny GenericArray musí podědit tuto šablonu a vyplnit sebe jako Potomek. Protože se stále pohybujeme na úrovni šablon, nevadí, že výsledkem instanciování je specifická verze jak potomka tak na míru vytvořená varianta GenericArray. Sečíst prvních N prvků bude snadné:

template<class Potomek, class TypPrvku>
TypPrvku SectiPrvnichN(const GenericArray<Potomek,TypPrvku> &pole, int pocet)
{
TypPrvku vysledek = 0;
for (int i = 0;i<pocet;pocet++) vysledek = vysledek + pole[i];
return vysledek;
}

Všimněte si, že jsem si dovoli výsledek vracet. To protože, že mohu použit TypPrvku. Tuto funkci nemohu zavolat na nic jiného, než na třídu typu GenericArray. Avšak podle toho, kterého potomka použiju, tak se mi nastavi parametry Potomek a TypPrvku (to co při běžné pozdní vazbě není možné - je vidět, že vznik N předků pro potomky má nějaké výhody, protože jsme schopni z potomka odvodit, s jakými parametry vznikla třída předka)

class NormalniIntPole: public GenericArray<NormalniIntPole, int>
{
...
};

NormalniIntPole intpole();
... //nastaveni prvku v poli
int vysledek = SectiPrvnichN(intpole, 10);

Právě GenericArray je rozhraní na úrovni šablon.

Invoker

CRTP přináší do programování šablon další užitečné možnosti. Bylo by ale vhodné ho zabalit do nějaké třídy, které CRTP bude zapouzdřovat.

Pro vyvolávání metod a atributů podle CRTP jsem si vymyslel metodu Invoke(). I taky tak trochu proto, že to hodně připomíná vyvolávání duchů. Základní třída je součástí nějakého potomka, je jím obklopena, ale nevidí jí. Musí metody vyvolávat, jako duchy (které jsou taky kolem nás, ale nevidíme je :-) ).

Třída, která implementuje metodu Invoke se jmenuje Invoker (vyvolávač)

   template<class  Derived>
class Invoker
{
public:
Derived &Invoke() {return static_cast<Derived &>(*this);}
const Derived &Invoke() const {return static_cast<const Derived &>(*this);}
};

Použití Invokeru je jednoduché. Stačí jej pouze podědit.

template<class Derived>
class InvokeExample: public Invoker<Derived>
{
public:
void foo() {Invoke().foo();}
int getState() {Invoke().state;}
};

Třída definuje rozhraní s funkcemi foo() a getState(). Funkce foo() volá implementaci této funkce v potomkovi. Funkce getState() vyzvedává hodnotu proměnné state z potomka(mimochodem, toto pozdní vazbou neuděláme).

Veškeré vyvolávání metod a atributů potomka se děje pomocí funkce Invoke(). Dokonce vyvolat celou instanci potomka lze prostě tak, že zavoláme Invoke() a vrácenou referenci si zapamatujeme.

Ještě si ukažme použití šablony.

template<class T>
void example(InvokeExample<T> &x)
{
if (x.getState() == 0) x.foo();
}

Jednoduché, ne?

Poznámka
Všimněte si, že funkce foo volá potomka a stejnou funkci. Co se stane, když potomek nebude funkci implementovat? Zcela jistě dojde k nekonečnému rekurzivnímu volání vedoucí k vyčerpání zásobníku. Tahle chyba by se projevila až v runtime, což je celkem hloupé. Lze to řešit tak, že potomek implementuje funkce pod stejným názvem, ale s použitím prefixu nebo suffixu. Například může implementovat funkci foo() jako foo_() nebo jako implFoo(). Poslední verze překladačů však dokáží rozpoznat nekonečnou rekurzi a takový kód zpravidla odmítnou přeložit.

Závěr

Použití Invokeru, potažmo CRTP může přinést další možnosti při psaní šablon. Zjednodušuje používání dědičnosti u šablon. A v neposlední řadě, obchází nutnost pozdní vazby při použití polymorfismu u šablon.

Vyzkoušejte a uvidíte.

vytvořeno: 8.4.2007 17:43:54, změněno: 8.4.2007 17:43:54
Jsou informace v článku pro Vás užitečné?
  • (1)
  • (2)
  • (2)
  • (0)
  • (0)
Nick nebo OpenID
Vzkaz
 
26.8.2015 12:25:54

new balance

new balance taiwan ̨
9.7.2015 14:13:09

conversę

converse
28.5.2015 20:39:43

michael kors handbags

michael kors sale
michael kors handbags http://michaelkorssg.gaytalkradio.org/
2.7.2014 23:59:20

p3r8IxDphJd

Ahojky Moniko. Kouke1m, že jsi si trošku upravila stre1nky. Jsou moc hezke9. Dnes mě mimo jine9 zaaujla ta fažasne1 deka. Pochopila jsem to do te9 doby, jak ji ušedt a rozstředhat, ale nened mi jasne9, co způsobilo to zkrabacened. Odkaz na blog, kterfd jsi dala je v angličtině, což je pro mě proble9m. Nemůžeš mi poradit? Dedky moc. Jestli už neme1š můj email, klidně pedsni na moje stre1nky. I tam ho ale najdeš. Měj se slunedčkově. Jitkawww.hutule.mypage.cz

Podobné články

Tanečky na obecném poli (GenericArray)

Jak sloučit dvě pole, aniž by bylo třeba alokovat jediný bajt? Jak smazat dvě hodnoty z prostřed pole, aniž by se muselo pole fyzicky posouvat? Jak vyzvednout subpole? Jak přerovnat pole bez fyzického přesunu prvku?

Automatické klonování objektů v C++

C++ nenabízí standardní prostředky jako klonovat objekty. Pouze kopírovací kontruktor, který nám však v případě polymorfních objektů moc nepomůže. Naše "lenost" nám však pomůže nalézt řešení.

Úvod do obecného OOP 3. díl

Dědičnost

Šablony: Nastavování vlastností tříd

Svět šablon v C++ je velmi zajímavý, pro znalce dokonalý, pro začátečníky šílený a nepřehledný. Následující článek pojednává o dalším kouzlu se šablonami

Multiple Interface a Instance Factory

Používáme-li v C++ dědičnost a polymorfizmus, často řešíme problém, jak psát operace nad objekty, jejiž implementace se liší podle typu potomka, bez toho aniž bysme zasáhli do Base třídy.
Reklama: