Úvod do U++ (část 1)
1. Utimate++ - politicky nekorektní platforma
1. Co je U++
Pod nabubřelým názvem Ultimate++ (krátce U++) najdete cosi, co by se dalo stručně nazvat jako "C++ jinak". Jedná se o kompletní platformu pro vývoj aplikací, sestávající z knihovních modulů, metodiky správy těchto modulů a vývojového prostředí ("TheIDE"), které této modularizaci rozumí (zapomeňte na knihovní moduly .lib, .so a makefile soubory).
Počátky U++ se datují do doby přibližně před 9 lety. Důvodem vzniku bylo naše znechucení z existujících nástrojů a knihoven pro vývoj komerčních databázových client-server SQL aplikací pod MS Windows a Oracle. Vývoj začal jako malá knihovna pro práci s Oracle pod MFC. Jak už to bývá, postupem času se rozrůstal s potřebou řešit další úlohy a s nápady na lepší řešení problémů již existujících.
Někdy kolem roku 2002 byly z kódu odstraněny poslední vazby na MFC a začaly práce na podporu X11 (a tedy Linuxu a FreeBSD). Od konce roku 2003 U++ existuje jako open-source projekt pod BSD licencí na www.sourceforge.net.
Ačkoliv je U++ často chápán jako GUI knihovna, ve skutečnosti pokrývá prakticky všechny běžné úkoly, na které lze použít "kompilované" jazyky. A tak vedle rozsáhlých desktopových aplikací, často pracujících s databázemi a grafikou, se U++ používá třeba i pro webové servery (například http://www.wmap.cz/atlaszp) nebo pro ryzí systémové nástroje, jako je COFF linker (rychlý linker “uld”, kterým je nahrazen ld.exe v U++/mingw distribuci).
Mimochodem, ačkoliv je U++ open-source a zadarmo, je pod BSD licencí a nic nebrání jeho komerčnímu využití. Naopak takové použití vítáme; celé U++ je vlastně určeno tak trochu pro profesionální programátory stejné jako my, pro něž je programování nejen zdrojem příjmů, ale i vášní a zábavou.
2. Předkrm
Hlavním cílem U++ je co nejvíc zjednodušit práci programátora, čehož se snažíme dosáhnout co možná nejjednodušším a nejpřehlednějším kódem. Zkusme si to pro začátek demonstrovat na jednoduchém příkladu:
- GUI aplikace má za úkol zobrazit, kolik dní se nachází mezi dvěma zadanými daty. Výsledek bude průběžně zobrazován po každé změně data:
- V "layout designeru" TheIDE se nejprve vytvoří vizuální návrh okna aplikace:
Kód aplikace:
#include <CtrlLib/CtrlLib.h>
#define LAYOUTFILE <Days/Days.lay>
#include <CtrlCore/lay.h>
class Days : public WithDaysLayout<TopWindow> {
public:
void Compute();
typedef Days CLASSNAME;
Days();
};
void Days::Compute()
{
result = IsNull(date1) || IsNull(date2) ? "" :
Format("There is %d day(s) between %` and %`",
abs(Date(~date1) - Date(~date2)), ~date1, ~date2);
}
Days::Days()
{
CtrlLayout(*this, "Days");
date1 <<= THISBACK(Compute);
date2 <<= THISBACK(Compute);
}
GUI_APP_MAIN
{
Days().Run();
}
3. Trocha filozofie a politicky nekorektních názorů
Obvyklá kritika U++ vychází z prvního pohledu, podle nějž U++ "znovuvynalézá kolo" a implementuje vlastní třídy jako zbytečné náhražky standardní C++ knihovny.
Proč děláme takovou hloupost? Stručná, byť možná trochu nesrozumitelná, odpověď na tuto otázku by se dala shrnout do věty: "Protože STL vás nutí si přát, aby C++ mělo automatickou správu paměti (garbage collector - GC)".
Problém je v tom, že do STL kontejneru můžete vkládat pouze objekty, které mají definovanou kopii. Navíc, pokud aspoň trochu dbáte na rychlost aplikace, musí tato kopie pokud možno být jednoduše proveditelná (rychlá).
To v praxi znamená, že do STL kontejneru je možné efektivně ukládat fundamentální typy, možná ještě tak řetězce a pár dalších jednoduchých typů. Všechno ostatní lze uložit pouze nepřímo pomocí ukazatele. Tím se ztrácí jedna ze základních výhod, kterou používaní kontejnerových šablon přináší - automatická správa paměti.
Mnohé C++ knihovny (např. boost) a konečně i nový C++ standard se snaží řešit tento problém prostřednictvím tzv. chytrých sdílených ukazatelů (boost::shared_ptr), které počítají odkazy na daný objekt a uvolní jej ve chvíli, kdy zmizí poslední odkaz. Jedná se vlastně o nedokonalou nápodobu automatické správy paměti (nedokonalou proto, že neumí uvolnit cyklické odkazy).
Náš názor je ten, že tento způsob řešení je zcela hloupý - vnáší zbytečný zmatek do všech rozhraní i implementací (jaký ukazatel má funkce vracet, chytrý, nebo normální?) a navíc zbytečně plýtvá výkonem CPU - počítání referencí konečně není zadarmo, zvláště když má program běžet ve vícevláknovém prostředí. Kromě toho tento přístup zamlžuje "životnost objektu"; často není jasné, kdy vlastně určitý objekt zaniká. Je tak obtížné používat destruktor k některým akcím, ke kterým by logicky mohl a měl být použit (třeba zavírání GUI oken na obrazovce).
Shrnuto a podtrženo, v U++ se snažíme dodržovat tyto zásady:
- Správa paměti je implementační detail, na úrovni rozhraní nemá co dělat.
- delete je nízkoúrovňová operace, které by se měla používat co nejméně. Většina koncových aplikací v U++ ji nepoužívá vůbec.
- new má legitimní použití pouze ve velice speciálních případech, většinou souvisejících s polymorfií.
- Chytré sdílené ukazatele jsou to nejhorší, co mohlo C++ potkat.
4. Všechno má své místo
Předpokládám, že po přečtení předchozího odstavce většina zkušených C++ programátoru musí začít kroutit hlavou a namítat, jak je tedy možné psát programy v U++. Nuže:
- Naprostá většina objektů v U++ aplikaci je svázána s určitým blokem, přímo či nepřímo. Jinak řečeno, pro většinu objektu lze najít jednu konkrétní '}', která ukončuje jeho existenci.
- Samozřejmě často není předem známo, kolik objektů daného typu má v určitém bloku vzniknout, případně jakého odvozeného typu mají být. Právě k řešení tohoto problému slouží U++ kontejnery, jejichž podstatným rozdílem oproti standardu je možnost jednoduše ukládat objekty bez požadavku na existenci "kopírovací operace".
- Domníváme se, že striktní dodržování tohoto přístupu činí C++ jazykem stejně efektivním či spíše efektivnějším, než jazyky používající automatickou správu paměti, jako je Java nebo C#. Tyto jazyky jsou snad schopny se postarat o vaši pamět, U++ se ovšem stará o všechny použité zdroje, a to velmi deterministickým způsobem.
Poznámka na okraj: výše uvedené pochopitelně neznamená zákaz používání ukazatelů v U++ - ukazatele zde ovšem slouží k ukazování na objekty, ne jako jejich reprezentace. Samotný objekt by měl být vždy svázán s určitým blokem buď přímo, nebo prostřednictvím kontejneru.
5. U++ kontejnery
V praxi je to trochu složitější, protože některé typy lze ukládat efektivněji "přímo" (podobně jako v STL), a byla by škoda tuto možnost obětovat. Z tohoto důvodu existují dva základní poddruhy U++ kontejnerů:
- Poddruh Vector nabízí velice efektivní způsob uložení hodnot omezené množiny typů. Omezující požadavky na ukládaný typ jsou u něj ještě silnější, než ve standardní knihovně, především se vyžaduje, aby byl tzv. Moveable; to ve stručnosti znamená, že nesmí obsahovat vnitřní odkazy na sama sebe. Většina hodnotových typů v U++ tento požadavek splňuje (ve skutečnosti nebývá zvlášť těžké toho docílit).
- Poddruh Array nemá na typ ukládaného objektu požadavky vůbec žádné. Dokáže ukládat i polymorfní objekty odvozené od základního typu v parametru šablony. Díky tomu je v U++ zcela normální například kontejner GUI elementů (
Array<EditInt> integer_editors) a lze jej klidně setřídit pomocí šablonové funkceSort. Něco takového by ve standardní knihovně vyžadovalo použití kontejneru ukazatelů (std::vector<EditInt *>) nebo nějakou formu chytrých sdílených ukazatelů, obojí ovšem zvyšuje složitost kódu a druhé řešení je navíc méně efektivní z hlediska výkonu.
6. Utržené kontejnery
Často je pro logiku programu výhodné přesunout obsah kontejneru na jiné místo bez zachování zdrojových dat, například u návratové hodnoty funkce (automatická proměnná uvnitř funkce, pomocí níž byl kontejner naplněn, při opuštění funkce končí svou platnost a je zbytečným mrháním výkonu celý obsah kontejneru kopírovat). Výsledkem takové operace je naplněný cílový kontejner, zatímco zdroj se nachází v "utrženém" stavu a lze na něj aplikovat pouze omezenou sadu operací (smazání a přiřazení či utržení obsahu jiného kontejneru). Utržení je přitom implementačně nenáročná a rychlá operace.
V praxi se logika programu uspokojí s operací utržení ve většině případů místo skutečného kopírování obsahu kontejnerů; U++ tedy pro tuto operaci používá běžný operátor přiřazení (operator =), zatímco pro kopii obsahu se používá <<=; zároveň je použití operátoru přiřazení nutné aby se trhací operace použila pro návratové hodnoty funkcí. To se může zdát poněkud matoucí a bývá to občas kritizováno jako možný zdroj chyb v programech. Ve skutečnosti se s chybami vzniklými porušením "trhací sémantiky" setkáváme zřídka a bývá snadné je odhalit; zdrojový kontejner ví, že se nachází v "utrženém" stavu, a nelegální práce s jeho obsahem je v ladicím módu detekována jako běhová chyba.
7. Příště
V tomto článku jsme probrali základní koncepty návrhu U++; v příští části se podíváme, jakým způsobem se filozofie U++ projevuje při vývoji GUI aplikací.

Diskuze