Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!
Zadejte Vaši e-mailovou adresu:
Kamarád fotí rád?
Přihlas ho k odběru fotomagazínu!
Zadejte e-mailovou adresu kamaráda:
C/C++
Kopírování velkých objektů v C++
27. května 2002, 00.00 | Po menší odmlce je tady opět další díl seriálu objektově orientované programování. Dnes se podíváme na problematiku kopírování velkých objektů. Kopírování lze někdy potlačit, aniž bychom použili referenci, nebo ukazatel.
Dnes si ukážeme jak mít plně pod kontrolou kopírovaní instancí. Představme
si situaci, kdy máme veliký objekt. Objekt je veliký například proto, že
obsahuje veliké pole. Můžeme se dostat do situace, kdy je nutné vytvořit
jeho kopii. Například jej musíme předat funkci, nebo metodě jako parametr.
Kopírovat objekt je neefektivní. Ale předávat funkci, či metodě pouze ukazatel,
nebo referenci není vždy možné. V těle funkce nebo metody můžeme chtít provádět
s objektem nekonstantní operace, tedy operace které změní vnitřní stav objektu.
Kdyby byla funkci nebo metodě předána jako parametr reference (nebo ukazatel),
došlo by ke změně stavu i u originálu. Což může být nežádoucí.
Ideální by bylo, kdyby se objekt kopíroval pouze v případě, kdy je to potřeba,
ne hned při předávání objektu jako parametru. A úplně nejideálnější by bylo,
kdyby se objekt kopíroval jen v případě, kdy je to potřeba, a jen ta část
objektu, kterou je potřeba zkopírovat. Jak na to?
Pro přesnost musím jen podotknout, že ke kopii vlastně dojde vždy. Řeší se zde, jestli má být kopie hluboká, nebo plytká. Pojmy hluboká a plytká kopie objektu jsme si vysvětlili ve článku Kopírovací konstruktor v C++. Je-li objekt velký (Například obsahuje velké pole), vytváření hluboké kopie je velmi náročné na čas (a také na paměť). Naopak plytká kopie je někdy nepoužitelná. Vytvoříme tedy třídu, jejíž objekty budou vždy kopírovány jako plytká kopie, ale v případě potřeby dojde dodatečně k vytvoření hluboké kopie. Budeme mít vlastně vytvořený objekt, který se bude kopírovat vždy jako plytká kopie. Až v případě potřeby se automaticky dodatečně vytvoří hluboká kopie.
Obvykle se volí postup, kdy se objekt vlastně rozdělí na dvě části. Na
část "obsahovou" a "přístupovou". Je tedy nutné vytvořit dvě třídy. Třída
"obsahových" objektů má jako své atributy data, jejichž kopírování chceme
mít pod kontrolou. Instance této třídy obsahují "velká" data. Programátor
by neměl mít na tyto objekty žádné reference, nebo ukazatele. Pracovat s
nimi by měl pouze pomocí "přístupových" objektů. Obsahový objekt v sobě
musí mít zapouzdřen čítač referencí. Musí vědět, kolik "přístupových" objektů
se na něj odkazuje. "Obsahový" objekt se bude kopírovat pouze v nejnutnějším
případě. Naproti tomu třída "přístupových" objektů bude jako svůj atribut
nutně obsahovat ukazatel na jeden "obsahový" objekt. Přístupový objekt bude
možné libovolně kopírovat, protože je malý. Programátor bude pracovat s "přístupovým"
objektem.
Mějme úplně obyčejnou třídu, jejíž instance budou pravděpodobně zabírat
velkou část paměti. Chceme ji předělat tak, aby jsme ušetřili zbytečné kopírování
takové instance. Obecně lze doporučit postup:
- 1) Vyjmeme z naší třídy všechny atributy, které dělají její instanci velikou. Dáme je do jiné třídy, kterou můžeme například nazvat stejně, jenom s dvěmi podtržítky na začátku. Dejme tomu, že jsme měli třídu Třída, nyní máme třídy Třída a __Třída. Třída je třída přístupových objektů, __Třída je třída obsahových objektů. O existenci třídy "__Třída" a o jejích instancích nemusí programátor používající přístupovou třídu vůbec vědět.
- 2) Třídě __Třída přidáme soukromý atribut udávající počet existujících referencí na instanci. Nazvěme si jej například ReferenceCount. Bude typu unsigned int. Dále přidáme veřejné metody, které zvýší, sníží, vrátí počet referencí. Pojmenujme si je například incrementReferenceCount, decrementReferenceCount, getReferenceCount.
- 3) Třídě __Třída vytvoříme kopírovací konstruktor a operátor = tak, aby vytvářeli hlubokou kopii. Dále by měl být k dispozici pochopitelně destruktor, který uvolní paměť a také nějaké jiné konstruktory. Ve všech konstruktorech nastavíme výchozí počet referencí na 1.
- 4) Třídě Třída přidáme ukazatel na instanci typu __Třída. Nazvěme jej například Objekt. Tedy Třída má atribut __Třída *Objekt. Měl by být soukromý.
- 5) Třídě Třída přidáme metodu, která odregistruje objekt. Nejprve sníží počet referencí na objekt, na který se odkazuje ukazatel Objekt. Provede to pomocí metody decrementReferenceCount. Je-li po zavolání metody decrementReferenceCount počet odkazů na objekt 0, potom jej zničí destruktorem. Metodu nazveme například free. Neměla by být veřejná. Měla by být soukromá, nebo chráněná.
- 6) Třídě Třída přidáme metodu, která dodatečně provede kopírování instance Objekt do hloubky. Metodu můžeme nazvat například copy. Je-li počet referencí 1, kopie není potřeba a metoda se ukončí. V opačném případě zavoláme metodu free (uvolnění starého objektu) a poté vytvoříme hlubokou kopii objektu Objekt například pomocí kopírovacího konstruktoru třídy __Třída.
- 5) Třídě Třída vytvoříme kopírovací konstruktor a operátor =, které vytvoří jen plytkou kopii instance Objekt. Navíc zavolají objektu Objekt metodu incrementReferenceCount.
- 6) Třídě Třída vytvoříme destruktor, ve kterém zavoláme metodu free.
- 7) Všechny metody (kromě konstruktorů, destruktoru, operátoru =, metod copy a free) ze třídy Třída "přesuneme" do třídy __Třída.
- 8) Pro všechny metody, které jsme v bodě 7 přesunuli vytvoříme ve třídě
Třída metody, které "přesměrují" volání
na objekt Objekt. Jedná-li se navíc o metodu,
která mění vnitřní stav objektu, zavoláme na jejím začátku metodu copy.
Ukázka "přesměrování" metody, která nemění vnitřní stav objektu:návratová_hodnota Trida::metoda(parametry)
{
return Objekt->metoda(parametry);
}návratová_hodnota Trida::metoda(parametry)
{
copy();
return Objekt->metoda(parametry);
}
Uveďme si velice jednoduchý příklad. Vytvoříme velice jednoduchý příklad třídy, která bude zapouzdřovat velmi rozsáhlé pole.
|
Nyní si představme hodně zvláštní funkci, která jako svůj parametr bude mít objekt naší třídy a příznak. Je-li příznak nulový, funkce vrátí součet prvků v poli objektu naší třídy. Je-li příznak nenulový, přičte k prvním deseti prvkům příznak a vrátí součet prvků v poli.
|
Je zřejmé, že v této funkci nemůže být parametr objekt třídy Třída předáván referencí, nebo ukazatelem. Došlo by ke změně prvních deseti prvků i u originálního objektu. Na druhou stranu je zbytečné vytvářet na zásobníku kopii objektu v případě, že parametr příznak bude 0. Řešením je potlačit kopírování objektu. Dodržme postup, který jsem uvedl v osmi bodech a vytvořme nové dvě třídy. Výsledek bude vypadat takto:
|
Nyní je zřejmé, že k hluboké kopii, tedy ke kopírování velkého pole dojde pouze v těle metody copy. Ve zmiňované funkci fce nedojde ke kopírování, jestliže je parametr příznak roven 0.
V druhém příkladě bude programátor s třídou Třída zacházet normálně jako-by zacházel se třídou Třída v prvním příkladě.
V mnoha knihovnách se tato "technika řízeného kopírování" používá a programátor používající danou knihovnu o tom možná ani neví. Jen někde v dokumentaci může být třeba napsáno, že objekty nějaké třídy používají řízené kopírování, kopírování až při potřebě nebo něco v tom smyslu. Mezi nevýhody patří zejména fakt, že se musí už při tvorbě třídy brát v úvahu fakt, že kopírování bude řízené. Je třeba vytvářet pomocné třídy a tím se zvyšuje množství zdrojového textu. Nelze vytvořit nějaký obecný program, který by dokázal předloženou třídu "přetransformovat" podle zmiňovaných osmi bodů na požadovaný výsledek. Stejně tak není možné vytvořit obecnou šablonu, které by jsme předložili "normální" třídu jako parametr a ona by nám vytvořila typ, který by měl řízené kopírování. Prostě vše musí udělat programátor ručně.
Velmi často se ale vyskytují ve třídách atributy, které jsou jednorozměrná pole (i v ukázkovém příkladu v tomto článku). Taková pole mohou být velice objemná. Tím se kopírování objektů ,obsahujících taková pole, stává velmi náročné. Zde by se dala vytvořit šablona, jejíž parametr by byl typ prvku v poli. Pole by se kopírovalo pouze v případě, že by se do pole zapisovalo. Pro čtení z pole není nutné provádět kopii. Takovou šablonu vytvořím a dám k dispozici ve svém příštím článku. Pomocí šablony, kterou předložím lze například pole rozdělit na několik částí. V případě zápisu do pole se provede kopie pouze té části, které se zápis týká. V příkladu, kde si ukážeme použití šablony vytvoříme matici, která bude při zápisu kopírovat pouze řádky, do kterých bude vepisováno.
Všem, které tento článek zaujal doporučuji přečíst si článek následující. Bude bezprostředně navazovat na tento článek.
Obsah seriálu (více o seriálu):
- Základy OOP v C++: Od C k C++
- Základní pojmy objektově orientovaného programování
- Vytváření tříd, instance třídy, zasílání zpráv v C++
- Vytváření instancí - konstruktory, destruktory
- Kopírovací konstruktor v C++
- Jednoduchá dědičnost v C++
- Časná versus pozdní vazba - úvod do polymorfismu v C++
- Polymorfismus - dokončení
- Vícenásobná dědičnost v C++
- Vícenásobná dědičnost v C++ - opakovaná dědičnost
- Vícenásobná dědičnost v C++ - volání konstruktorů a destruktorů
- Přetěžování operátorů v C++ 1.díl
- Přetěžování operátorů v C++ 2. díl
- Vstupní a výstupní operace pomocí datových proudů v C++
- Přetěžování operátorů << a >> pro datové proudy v C++
- Neformátovaný vstup a výstup v C++
- Paměťové proudy v C++
- Prostory jmen v C++
- Řetězce v C++
- Výjimky v C++
- Výjimky v C++ - výjimky tvoří dědičnou hierarchii
- Výjimky v C++ - dokončení
- Dynamická identifikace typů v C++
- Přetypování v C++
- Problémy s typy při vícenásobné dědičnosti
- Šablony funkcí v C++
- Šablony datových typů v C++
- Vnitřní typy u parametrů šablon, vnořené šablony v C++
- Pole s libovolným intervalem indexování v C++
- Datové kontejnery v C++ - Úvod do STL
- Vector - datový kontejner v C++
- Iterátory v C++
- Šablona vector v C++ a iterátory
- Asociativní pole v C++
- Množina v C++
- Funkční objekty v C++
- Standardní funkční objekty v C++
- Úvod do standardních algoritmů v C++
- Kopírovací a přesouvací algoritmy v C++
- Vyhledávací algoritmy v C++
- Skenovací (prohlížecí) algoritmy v C++
- Transformační algoritmy v C++
- Řadící algoritmy v C++
- Halda v C++
- Standardní algoritmy v C++ - dokončení
- Automatické ukazatele v C++
- Inteligentní ukazatel - čítač referencí v C++
- Použití čítače referencí v C++
- Kopírování velkých objektů v C++
- Řízené kopírování prvků v poli v C++
- Dokončení seriálu objektově orientované programování v C++
Diskuse k článku
-
25. listopadu 2012
-
30. srpna 2002
-
10. října 2002
-
4. listopadu 2002
-
12. září 2002
-
25. listopadu 2012
-
28. července 1998
-
31. července 1998
-
28. srpna 1998
-
6. prosince 2000
-
27. prosince 2007
-
4. května 2007