Nejnovější články z této kategorie

Servis

Mirek Fídler - Projekty (6:00 | 29.5.)  Jagg.cz  linkuj.cz

Ú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:

Náhled okna

  • V "layout designeru" TheIDE se nejprve vytvoří vizuální návrh okna aplikace:

Náhled The IDE

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é funkce Sort. 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