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
Vracíme z funkce objekty - Skrytí konstruktoru - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Vracíme z funkce objekty - Skrytí konstruktoru

Skrývání konstruktoru může být důležité v případě, že potřebujeme v objektu použít Pimpl Idiom. Objekty totiž mohou vznikat i z datových typů, které nechceme zveřejňovat


Motivace

Motivací ke skrývání konstruktoru byla moje vlastní implementace objektového GUI pod Windows. Mnoho objektů v knihovně, které jsou veřejně dostupné, používají Pimpl Idiom techniku, aby zakryly implementační závislosti na platformě Windows. Jenže je zde malý problém... Často potřebuji některý objekt inicializovat nějakým způsobem, který je implementačně závislý.

Jenže konstruktor tak snadno neskryji. Ten prostě bude vidět v H souboru a nic s tím neudělám. Tak jak to obejít.

Funkce jako konstruktor

V minulém díle jsme si ukázali, že volání funkce, která vrací objekt, může často dobře nahradit konstruktor. Ukažme si příklad z praxe.

Window msg=SpecialWindows::CreateMessageBox();
msg.SetVisibility(true);

Účelem programu je vytvořit speciální variantu okna, představující okno se zprávou, které je již v systému vytvořeno (a třeba používá nějaký standardní design) a toto okno následně zobrazit. Na první pohled to vypadá tak, že třída Window je jen nějaký wrapper, a že obsahuje ukazatel na interní struktury popisující okno. Ale nemusí to být pravda (a zrovna v téhle knihovně to tak není). Je pravdou, že instance Window obsahuje někde ukryté hWnd, které zkrývá pomocí Pimpl Idiomu, nicméně, ten je realizování přímo v instanci (o této technice někdy příště) a navíc obsahuje spoustu další pomocných proměnných. Je pravdou, že instance má pevnou velikost a všechna dceřiná okna jsou následně spravována nějakým dynamickým seznamem (vektorem).

Zkusme nahlédnout do implementace funkce CreateMessageBox()

Window SpecialWindows::CreateMessageBox()
{
// ... inicializace nějakých struktur ...
HWND hWnd=CreateWindow(....);
return Window(hWnd,...); //ha! - konstruktor
}

A je to jasné. Výslednou instanci, kterou následně předáme volající třídě musíme nějak zkonstruovat. A tady máme problém. Protože ke konstrukci okna využijeme typy viditelné pouze uvnitř knihovny, jenž chceme skrýt vně, a tady potřebujeme konstruktor přijímající typ HWND. Konstruktor přitom musí být viditelný v H souboru a skrýt HWND se nám tedy nepodaří.

Jak to obejít?

Makro

Ukažme si jednoduchý způsob, který ale nebudeme používat.

Do definice třídy, v níž skrýváme konstruktor, dopíšeme definice nějakých maker.


#ifndef _WINDOW_HIDDEN_CONSTRUCTOR_
#define _WINDOW_HIDDEN_CONSTRUCTOR_
#endif

class Window
{
/*...*/
public:
_WINDOW_HIDDEN_CONSTRUCTOR_ //všechny skryté konstruktory
Window(const char *title,...); //běžný konstruktor
/*...*/
};

Do implementace CPP pak můžeme napsat.

#define _WINDOW_HIDDEN_CONSTRUCTOR_ Window(HWND hWnd /*, ...*/ ); //
#include "Window.h"

Princip je asi všem jasný. Konstruktory které jsou pro "interní použití" jsou v H souboru vidět jen tehdy, když se překládá příslušné interní CPP. Vně knihovny je makro prázdné. Překladači tato nesourodost nevadí, konstruktor je totiž z jeho pohledu obyčejná členská funkce a u těch platí, že jejich množství nemá vliv na finální ("datovou") podobu objektu (narozdíl od virtuálních funkcích).

Nicméně bych tuto techniku nedoporučoval používat. Ukážeme si, že existují techniky, které toto řeší více céčkovsky a tudíž nepotřebují berličku preprocesoru.

Nekompletní třída

Jednoduchou a elegantní technikou je použití nekompletní třídy

class InitClass; //nekompletní třída

class MyClass
{
public:
MyClass(const InitClass &init);
}

Nekompletní třída není definovaná vně knihovny, pouze uvnitř. Přesto lze H soubor použít a přeložit. Překladači stačí pouze příslib, že taková třída někde existuje a fakt, že se předává referencí (pointer by taky prošel). K popisu rozhraní mu to bohatě stačí.

Výhodou tohoto řešení je jednoduchost. Používám jej často u malých nekomplexních třídách. Je tu i pár nevýhod. Jednou je fakt, že zveřejňují všem informaci o nějaké vnitřní třídě, což nemusí být vždy dobrý nápad. Další problémem může být i to, že pokud mám víc takových konstruktorů, pak musím pro každý z nich založit takovou třídu. Další problém může nastat kolizí jmen, ale to se snadno vyřeší použitím namespaců.

Kopírovací konstruktor

Kopírovací konstruktor lze využít v kombinaci s děděním. Pokud si interně podědíme třídu Window tak, abysme si mohli instanci připravit v této třídě a pak výslednou instanci skopírujeme a předáme jako výsledek, je to taky způsob, jak skrýt konstruktor. Pěkný na tom je, že o existenci skrytého konstruktoru se ze strany rozhraní vůbec nic nedozvíme.

Nevýhodou ale je právě to kopírování. To zpravidla provedeme přímo u příkazu return uvedením proměnné jenž je typu potomka třídy. Pokud se jedná o nějaká okna na obrazovce, či jiné grafické elementy, nebo obrovské objekty, kopírování nám nemusí zrovna vyhovovat. Konečně, jeden příklad za všechny

WorldMap map=WorldMap::LoadMap(".\\mapfile.map"); //dejme tomu, že soubor ma 50MB

Ve výše uvedeným příkladě bysme s kopírovacím konstruktorem bez použití dalších technik (mělká řízená kopie) nepochodili.

Konstukce pomocí šablony

A dostáváme se do jakéhosi pokročilého stupně. Zkusíme použít šablonu.

class Window
{
...
public:
template<class InitFunctor>
Window(const InitFunctor &init) {init(*this);}
//v případě kompikaci s kopírovacím konstruktorem
//enum InitByTemplateEnum {InitByTemplate};
//template<class InitFunctor>
//Window(InitByTemplateEnum tag, const InitFunctor &init) {init(*this);}
};

V tomto případě otevíráme třídu všem tzv. inicializačním funktorům. Inicializační funktor má předepsaný protokol, jakým musí třídu inicializovat (zde je to pouhým zavoláním operator(), ale protokol může být složitější, například v něm mohou figurovat konstruktory member proměnných a předků).

Uvedená technika má výhodou v širokém použití. Stačí nám jeden takovýto konstruktor a funktorů si můžeme vymyslet libovolné množství. Výhodou i nevýhodou může být to, že stejně možnosti má i uživatel knihovny a může je tedy využívat, ale má také k dispozici nebezpečný nástroj k tvorbě nekonzistentních instancí. Pokud se však nedostane ke skrytým proměnným (pimpl idiom), nemůže si s tím nějak moc výrazně pomoci.

Nevýhodou může být i problém s přístupovými právy. Inicializační funktor, pokud přistupuje přímo k členským proměnným třídy buď musí být friend (pak ale bude zveřejněno jeho jméno), nebo proměnné musí být veřejné (což se nám nemusí hodit do krámu). Může také být deklarovan uvnitř potomka třídy, pak by mohl přistupovat ke všem chráněným atributům. Poslední možností je, aby funktor volal statické členské funkce potomka třídy, pak také může přistupovat ke všem chráněným atributům. Je-li třeba přistupovat k soukromým atributům, nezbývá nic jiného, než deklarace friend.

Závěr

Vracení objektu z funkce nám umožňuje deklarovat konstrukční funkce, jejichž účelem je konstruovat objekty nějakým speciálním způsobem. Můžeme si vybrat jeden ze způsobů, jak skrýt konstruktor.

Samozřejmě, že můžeme konstrukční funkci navrhnou tak, aby vracela ukazatel na instanci. V tomto článku jsem však chtěl ukázat, že to lze i bez toho. Vracet ukazatel má výhodu v tom, že objekt můžeme zkonstruovat "normálně" a pak jej upravit a vrátit ukazatel. To právě nejsme schopni udělat pokud vracíme objekt (i když, nové překladače už umí i toto optimalizovat). Úkolem inicializačního funktoru je právě provést tyto úpravy během konstrukce, takže můžeme využít optimalizace při vracení objektu z funkce. A také se vyhneme nutnosti řešit alokaci paměti pro výsledek, to nám právě zajístí překladač vytvořením tzv. komůrky (víz předchozí díl).

vytvořeno: 30.10.2006 10:34:19, změněno: 30.10.2006 10:34:19
Jsou informace v článku pro Vás užitečné?
  • (0)
  • (1)
  • (1)
  • (0)
  • (0)

Podobné články

Vracíme z funkce objekty - praktická ukázka

V článku Vracíme z funkce objekty jsem ukazoval, jak probíhá návrat objektu z funkce a jak toto optimalizovat tak, aby se minimalizovalo používání kopírování. Teď si to ukážeme prakticky - použil jsem gcc bez optimalizací

Vracíme z funkce objekty - problémy

Při praktickém používání technik Vracíme z funkce objekty narazíme na nečekané a možná nelogické problémy.

Jak provést operaci až po příkazu return

Pokud optimalizujeme kód tak, abychom mohli využít výhod vracení objektu z funkce, narazíme občas na problém, kterak provést některé operace až po provedení příkazu return

Vracíme z funkce objekty

Podívejme se blížeji na to, jakým způsobem je implementováno vracení výsledku z volání funkcí. Zboříme při tom všelijaké mýty

FastAllocPool - urychlení častých alokací a dealokací

Pokud v programu z nějakých důvodů potřebujeme často provádět new a delete nad některými třídami, můžeme zvýšit efektivitu těchto operací zavedením poolu předalokované paměti
Reklama: