Rukavicemi házím strašně nerad!

Jak jsem si v minulém dílu našeho objektového povídání liboval, že "jsem rád, že ... se polemika rozvíjí od ... debaty o konkrétním ... jazyce ... k obecné diskusi o objektovém programování", dnes je mi trochu smutno: z nějakého důvodu se pan Virius rozhodl držet se striktně C++ a Javy, zatímco já se upřímně snažím hovořit spíše o "statickém objektovém systému jako v C++ či Adě" nebo "dynamickém objektovém systému jako v ObjC, Javě, nebo třeba SmallTalku". Navíc se mi zdá, že se nám debata o objektech a neobjektech stáčí do hádání se, jako na školním dvorku: já mám delšího! Ale mně zase dál dostříkne!

Moc mne neláká rozebírat konkrétní rysy konkrétních jazyků. Ano, jsem skutečně přesvědčen, že Objective C je mnohem lepší, než C++ -- prostě proto, že sám běžně programuji velké komerční systémy v obou, a také vedu týmy, jež programují v obou, a mohu proto srovnat technické obtíže a problémy, jež se v obou případech objevují. Myslím si ale, že o tom naše debata není -- nebo by alespoň být neměla!

Je -- nebo měla by být -- o rozdílech mezi tím, co se pod názvem 'objekty' používá v jazycích typu C++ (nebo Ady, nebo -- podle předběžných informací -- C#), a mezi tím, co se pod názvem 'objekty' používá v jazycích typu Objective C (nebo Javy, nebo SmallTalku). Tyto rozdíly jsou hluboké a vedou k poměrně zásadně odlišným nárokům na analýzu a design. Jsem přesvědčen, že právě zdůraznit -- a v mezích možností osvětlit -- tyto rozdíly pro všechny, kdo programují nebo budou programovat v obou objektových systémech, je hlavní a jediný důvod, proč psát tyto řádky na Chip CD. Pohádat se "je-li lepší C++ nebo Java" (má-li takto položená otázka vůbec smysl -- podle mého soudu nikoli) se přeci s panem Viriusem můžeme v soukromí!

Protože však na druhou stranu nechci ignorovat náměty a připomínky pana Viriuse, pokusím se alespoň -- nakolik to je možné -- od sebe oddělit (podle mého subjektivního názoru rozumnou) diskusi o objektových systémech od (podle mého subjektivního názoru zde v Chipu ne tak rozumné) diskuse o vlastnostech konkrétních programovacích jazyků: "objektové" odstavce uvedu normálními titulky, zatímco "neobjektové" uvedu titulky zarovnanými k pravému okraji. Čtenář, který má zájem o objekty, ale nezajímají jej žabomyší války o ten který programovací jazyk , tedy může všechny odstavce s titulky vpravo klidně přeskočit.

Samozřejmě opět připojím starší články; v citátech využiji HTML odkazy vedoucí na původní text (a vložení odpovídajících značek, spolu s převodem z osmibitové češtiny specifické pro windows do obecně srozumitelného Unicode, jsou jediné změny, jež jsem v něm provedl).

Učit se, učit se, na to třetí si nevzpomínám...

Ačkoli "learning curve" toho kterého jazyka je bezpochyby nesmírně důležitá a významná věc, obávám se, že s objektovým systémem jako takovým má málo společného. Snad jen to, že jednodušší systém je vždy snazší na pochopení než složitější, takže pochopit systém postavený čistě na polymorfních objektech s polymorfními metodami a jen jednoduchou dědičností je snazší, než pochopit komplikovaný mechanismus ve kterém je něco statické, něco dynamické, něco virtuální, něco nevirtuální, dědičnost vícenásobná, a ještě to komplikuje košatý overloading. Ani to ovšem není o podstatě věci; proto titulek tohoto odstavce zůstává napravo.

...co považuji za velkou výhodu C++ ve srovnání s ostatními potomky jazyka C, jako je Objective C nebo Java. Programátor, který zná jazyk C, může prakticky ihned začít programovat v C++...

Až dosud jsem vždy pana Viriuse chválil za hlubokou znalost věci, z níž jeho poznámky vycházejí; tentokrát se však bohužel i mistr tesař utnul. Nemá valný smysl mluvit o Javě, protože to je jiný jazyk (přesto se k ní stručně vrátím na konci tohoto odstavce); stojí za to -- když už o tom mluvíme -- se ale zdržet u Objective C. Ve srovnání s ním totiž právě vynikne zbytečná komplikovanost C++ a zbytečně dlouhá doba, potřebná k jeho naučení! Těžko dokládat zkušenosti řady programátorů na omezeném prostoru tohoto článku; můžeme si však ukázat několik zcela konkrétních drobností:

void *class,*Class;
int this,id,self,super;

Tento úsek programu nelze přeložit v C++: "céčkař" při překladu překvapeně zjistí, že class či this jsou v C++ vyhrazená klíčová slova, a musí se vrátit ke zdrojovému textu, a nepříjemně a nepohodlně přejmenovávat. Nejen nepříjemně, ale navíc naprosto zbytečně -- úmyslně jsem použil i řadu identifikátorů, které jazyku C++ nevadí, ale vadily by jazyku Objective C -- kdyby ovšem jeho návrh byl stejně nedomyšlený, jako návrh Stroustrupův: self je v Objective C přesně totéž, jako this v C++; Class, id i super tam také mají specifický význam (podrobnosti lze nalézt v mém seriálu o API Cocoa). Jazyk Objective C však je navržen rozumně, aby zachovával kompatibilitu s C, takže výše uvedené promněnné v něm lze bez nejmenších problémů používat.

"Céčkař" ovšem v C++ narazí na mnohem hlubší problémy, než je pouhé rozšíření seznamu klíčových slov. Typickým příkladem jsou problémy s přetypováním -- v C je samozřejmé, že ukazatel je adresa, a typ pouze určuje, co na té adrese je. V C++ tomu tak není, což velice hezky dokládá fakt, že pouhá změna typu ukazatele (u ukazatelů na objekty při použití vícenásobné dědičnosti pro simulaci rozhraní, jež C++ nemá) mění i jeho obsah (tj. právě tu adresu)! Opět můžeme jako protipříklad vzít Objective C: v něm je, stejně jako v C, pointer prostě adresa, a jeho typ určuje co na té adrese je -- ať už to je číslo, buffer, nebo objekt. Adresa však je, stejně jako v C, pevně daná a neměnná.

Nebo jiná drobnost -- jedna z velmi šikovných věcí v C++ (jež v Objective C chybí, a je to škoda) je možnost deklarovat proměnné víceméně kdekoli: i "céčkař" toho brzy a rád začne využvat. Pak ale dříve či později napíše úsek programu typu

if (...) goto Label;
...
int i=5;
...
Label:...

A je to v háji, protože překladač C++ ohlásí chybu (a to i ve zcela korektním případě, kdy se proměnná 'i' za návěštím Label již nikdy nepoužije)!

Podobných problémů je v C++ mnohem víc -- ať už jde o jeho typovou rigiditu (nutnost exlicitního přetypování u void*, spousta zbytečných warningů použijeme-li enum jak jsme zvyklí z C), nebo třeba jeho přístup k linkování...

Abychom si rozuměli -- nevtrdím, že cokoli z toho je snad samo o sobě v nějakém smyslu "špatně", to docela určitě ne! Je však naprosto absurdní tvrdit, že velkou výhodou C++ oproti Objective C je to, že "céčkař" v něm může začít rovnou psát, když C++ zachovává vlastnosti jazyka C v nesrovnatelně menší míře než Objective C!

Velice podstatné také je, že převážnou většinu knihoven jazyka C lze používat bez problémů v i C++

Zatímco v Objective C můžeme využívat knihovny ANSI C (nebo Posix) kompletně. Neméně důležité je také to, že takové knihovny v něm -- bez ztráty času studiem jeho jazykových specifik -- může "céčkař" také vytvářet. V C++ tomu tak není ani náhodou: jen zkuste vzít obyčejný "céčkový" program, obsahující řadu exportovaných funkcí, přeložte jej překladačem C++, a zkuste výsledný modul slinkovat s knihovnami a moduly z překladače C (nebo Fortranu, nebo čehokoli jiného)! Má-li to fungovat, musíme napřed nastudovat C++ do takové hloubky, abychom se seznámili s deklarací "extern C" a musíme se ji naučit důsledně využívat; v Objective C nic podobného není třeba.

Ostatně, když už jsme u toho: jazyk C++ je navržen tak šíleně, že je dokonce snadno možné že modul, který v něm vytvoříme, nepůjde slinkovat ani s moduly/knihovnami z jiného překladače C++! Není to vůbec pouze teoretická možnost -- právě teď má můj tým programátorů značné problémy s tím, že nelze dokonce linkovat moduly ani z různých versí téhož překladače (obtíž spočívá v tom, že C++ nepoužívá pro linkování jméno, jež uvedeme ve zdrojovém kódu, ale jméno od něj nějak odvozené -- přičemž toto odvození může každý překladač dělat jinak!).

I když je Java ╨ alespoň na první pohled ╨ jednodušší než C++, přechod k ní je pro céčkaře složitější, neboť programátor musí ihned změnit způsob myšlení

No, tohle taky nebude tak úplně přesné. Jde o to, že změna způsobu myšlení je sice v Javě nutná, na tom jsme se shodli -- ale při příchodu z C++ (a ne z C), a právě kvůli objektům -- které "céčkař" stejně nepoužívá!

Naopak, při příchodu z C je na tom Java skutečně ještě o něco hůř než C++, ale rozdíly nejsou zase tak zásadní: v Javě se "céčkař" musí naučit používat pojmenované bloky namísto goto, pokud chce používat preprocesor, musí si jeho volání vyžádat explicitně před voláním překladače Javy... podobných drobností je víc. Jistě, v Javě oproti C řada věcí chybí, ale jen máloco je tam jinak než v C. To přechod podle mého soudu spíš usnadňuje než komplikuje: bezpochyby méně "céčkařových" problémů vyvolají neexistující ukazatele v Javě, než ukazatele v C++, na první pohled stejné jako v C -- mající však občas zásadně odlišnou sémantiku (při výše zmíněném přetypování)!

Pak se samozřejmě dostaneme k tomu, že je třeba se začít učit nové věci:  třídy, objekty, komunikaci mezi nimi... To se programátor jak v C++, tak v Javě prostě musí naučit; v Javě je to však mnohem, mnohem jednodušší a přehlednější (přinejmenším dokud se nedostaneme k anonymním třídám, a ty jsou -- jak jsem ukázal v předminulém textu věnovaném popisu vazby na uživatelské rozhraní -- stejně na nic).

Zkrátka a dobře -- základy programování zůstávají bez podstatných změn. A pak, namísto myšlení v pseudoobjekech C++ se céčkař, který přišel do Javy, naučí rovnou myslet v plnohodnotných objektech, takže žádná změna způsobu myšlení není zapotřebí.

Navíc se musí seznámit s řadou knihovních tříd, a přitom mu zkušenost z jazyka C není k ničemu ╨ s knihovnami začíná prostě od nuly.

To je naprostá pravda v JBuilderu nebo podobném prostředí. Ovšem, k dispozici jsou i rozumnější možnosti -- např. API Cocoa, jemuž se věnuji v samostatném seriálu, a jež nabízí standardní sadu knihovních tříd nezávislou na programovacím jazyce, stejně dobře použitelnou z Objective C jako z Javy (jako z WebScriptu, nebo v budoucnosti libovolného jiného objektového programovacího jazyka).

Polymorfismus

Většina operátorů je totiž binárních a my bychom proto při zpracovávání objektů v beztypových kontejnerech potřebovali polymorfizmus vzhledem k oběma operandům; přeloženo do jazyka volání metod to znamená, že bychom potřebovali pozdní vazbu jak vzhledem k třídě instance, které patří metoda, tak vzhledem k třídě parametru.

Hmmmm, musím se přiznat, že tohle tak úplně nechápu. Co by, propána, takový "vícenásobný polymorfismus" vlastně měl znamenat?!? Zprávu dostane vždy jeden objekt -- ty ostatní jsou jejími argumenty. Polymorfismus vzhledem k příjemci zprávy se projeví při jejím předání; polymorfismus vzhledem k argumentům se projeví při práci s nimi. Jestliže tedy např. "a+b" je ekvivalentní něčemu jako "a.operatorPlus(b)", pak se samozřejmě polymorfně vybere "vhodná metoda" operatorPlus podle konkrétní třídy objektu a, a až se v její implementaci bude nějak pracovat s parametrem b, budou opět jeho metody vybírány polymorfně. Zde žádný problém není a být snad ani nemůže.

Předpokládám, že pan Virius má na mysli trochu komplikovanější případ, kdy by se měla použít metoda nějaké třídy: "a+b" by tak bylo ekvivalentní něčemu jako "a.class().operatorPlus(a,b)" -- nebo, samozřejmě, "b.class().operatorPlus(a,b)". Ani tohle však není problém; jen je třeba v deklaraci operátorů určit, která třída se má použít. Tato ambivalence je ale naprosto stejná, jako ambivalence v minulém případě (kdy stejně dobře připadala v úvahu interpretace "a.operatorPlus(b)" jako "b.operatorPlus(a)"), a ačkoli mně jako uživateli Objective C připadá dost matoucí, uživatelé C++ s něčím velmi podobným léta spokojeně žijí.

V dalším textu znovu pan Virius z trochu jiného úhlu rozebírá ideu že polymorfismus nějak souvisí s dědičností. Ta je ovšem hluboce mylná; protože jsem to podrobně ukázal už v minulém článku, nebudu to znovu podrobně rozebírat (samozřejmě, pokud je něco v mém textu nesrozumitelné -- ať již Vám, Miroslave, nebo některému z čtenářů -- dejte mi vědět, a já věc vysvětlím podrobněji).

Pan Virius se však také pokusil zúžit obecnou problematiku beztypových objektů (o kterých jsem hovořil já) na beztypové kontejnery. Když už na ně přišla řeč, stojí za to se na ně podívat podrobněji:

Na druhé straně málokdy potřebujeme kontejner, do kterého chceme ukládat absolutně cokoli, od čísel přes okna po druhy ptakoještěrů. Typicky potřebujeme polymorfní kontejner, který obsahuje buď různé druhy oken, nebo ty ptakoještěry, ale ne obojí.

Je pravda, že kontejner "naprosto na cokoli" potřebujeme skutečně málokdy; není to ale zase tak výjimečná situace -- dosti běžné jsou např. slovníky, jež přiřazují nejrůznějším, navzájem nesouvisejícím objektům jména.

Na druhou stranu, nesmírně často se hodí kontejner, do kterého lze ukládat objekty logicky příbuzných typů, a navíc další kontejnery. To se ale z praktického hlediska moc neliší od kontejneru "absolutně na všechno", protože:

Velmi dobrým příkladem je třeba struktura SGML (či XML, jako triviální příklad bohatě stačí i HTML): to přeci není nic jiného než kontejner, který může obsahovat různé objekty (totiž textové řetězce nebo tagy), tagy opět samy mohou mít stejně složitou vnitřní strukturu... S beztypovými kontejnery typu NSArray nebo NSDictionary (viz opět můj seriál o API Cocoa) je triviální takovou strukturu implementovat. V C++ to však nijak snadno nejde -- musíme vytvářet umělé pomocné třídy typu "XMLAtom", "XMLString", "XMLTag",... jež zbytečně celou věc komplikují, a navíc nás nutí neustále převádět jeden objekt na druhý (protože do kontejneru nelze uložit přímo string, musíme jej převést na "XMLString", odvozený od stringu i "XMLAtomu"...).

Ovšem k tomu, abychom dosáhli stejného výsledku jako třeba v Javě, stačí vytvořit si vhodnou dědickou hierarchii s jedním společným předkem...

Jistěže, to je ten "XMLAtom". Kolik je to ale zbytečné práce navíc!  Já nevím, snad v C++ opravdu není zvykem využívat pokud možno beze změny jednou napsaný kód -- já aspoň vždy měl za to, že o tom by především mělo být objektové programování... Tohle jde ale zmíněnému principu dokonale proti srsti, protože do takového kontejneru nevložím žádný knihovní objekt, žádný z objektů tříd, jež jsem si sám naprogramoval dříve... pro každou zbytečnost musím vytvářet novou, další třídu:  mám objekt, representující binární data, a chci jej ukládat do kontejnerů? Dobrá, ale musím nejprve udělat nějaký "XMLData" a připravit patřičné konstruktory... a ještě se budu nejspíš muset zbytečně motat ve vícenásobné dědičnosti!

Naopak, do beztypového kontejneru v Objective C stejně snadno vložím dávno nezávisle připravený objekt třídy NSData. Za rok si třeba koupím 3rd party knihovnu, jež nabízí, co já vím, třeba objekty pro WML (jazyk, ve kterém jsou popisovány WAP stránky) -- v Objective C s jeho beztypovými kontejnery je budu moci ihned a bez nejmenších problémů zahrnout do mé dávno hotové implementace SGML. V C++ to nejspíš nebude možné vůbec, nebo -- v nejlepším možném případě -- to půjde díky šablonám po novém překladu mých již existujících knihoven, jež se SGML kontejnery pracují! Proboha...

A ještě obecně k polymorfismu, nad komplexně nereálným příkladem reálných a komplexních čísel (všechny citáty jsou z jednoho odstavce, odkaz je proto jen v prvém z nich):

Naštěstí C++ nabízí jinou cestu ╨ sice na pohled možná méně objektovou, ale o to elegantnější, která není založena na dědičnosti. Stačí ve třídě komplexních čísel definovat konstruktor, který lze volat s jedním parametrem typu reprezentujícího reálné číslo.

Pro začátek stojí za to se zmínit o tom, že v objektovém systému s "nesprávnou" hierarchií (kde Complex je dědicem Real) je takový konstruktor samozřejmým vedlejším efektem: reálná čísla samozřejmě takový konstruktor mají, konstruktory se samozřejmě jako všechny ostatní metody dědí, budou jej tedy mít i čísla komplexní.

Dále, ačkoli to není výslovně řečeno, zřejmě toto řešení předpokládá, že reálná a komplexní čísla nebudou ve společné dědické hierarchii? Ouha, to nese řadu dalších problémů -- co třeba kontejnery, obsahující čísla? Před chvilkou jsme si ukázali, že pro beztypové kontejnery objektových jazyků by to nebyl problém, avšak typované kontejnery C++ to nezvládnou! Ty pak přinejlepším samy využijí zmíněného konstruktoru, takže v praxi budeme mít reálná čísla uložena v kontejnerech jako komplexní, a nastane přesně to, co pan Virius popsal slovy "Představa reálného čísla, které s sebou nosí imaginární část obsahující vždy nulu, se mi vůbec nelíbí a programátor, který něco takového spáchá, nejspíš sklidí, co si zaslouží".

Tak otevřeme cestu k automatickým konverzím reálných čísel na čísla komplexní a nahradíme dědičnost.

Inu, neotevřeme, nenahradíme. Problém je znovu v tom, že pan Virius si -- zřejmě podobně, jako většina programátorů v C++ -- opět představuje triviální monolitickou aplikaci, již lze kdykoli celou znovu přeložit. Pro takové hračky však je opravdu jedno, v jakém jazyce je píšeme (nejpohodlnější bývá často awk, shell script, ve složitějších případech Prolog, ale to sem nepatří).

V praxi jsou ovšem systémy složeny z řady spolupracujících modulů a knihoven, z nichž některé znovu přeložit nemůžeme z principu věci (protože k nim nemáme k dispozici zdrojový kód) a u ostatních by to bylo krajně nepraktické (protože jich je mnoho). Chceme-li ale mít možnost využít jednou napsaný a přeložený kód, výše uvedené "řešení" s novým konstruktorem je prostě nanic: existující kód na knihovnách bude staticky pracovat s objekty Real (a žádnými jinými), a dědičnost nedědičnost, pro práci s objekty Complex jej nebudeme moci nijak využít! Ukažme si to na triviálním příkladu funkce, jež ke svému prvému argumentu přičte všechny ostatní (pro seznam objektů použijeme obecně známý mechanismus proměnných argumentů, ukončených nulovým ukazatelem; v praxi bychom samozřejmě spíše využili kontejner):

void addNumbers(id n,...) {
  va_list a;
  id o;
  va_start(a,count);
  while (o=
va_arg(a,id)) [n add:o];
  va_end(a);
}

Funkce je naprosto triviální, a je zřejmé, že bude korektně pracovat s jakýmkoli objektem, který dokáže korektně zpracovat zprávu add na místě prvého čísla n, a s jakýmikoli objekty, jež lze předat jako argument této zprávy, na místě dalších parametrů. Dokonce i v případě, kdy by programátor metody vůbec neuvažoval o její budoucí flexibilitě, a namísto typu id všude použil typ Real*, by vše fungovalo bez nejmenších problémů (právě díky tomu, že objekty jsou skutečně polymorfní černé skříňky, a typy jsou pouze dodatečná informace pro překladač).

Ekvivalentní funkce v C++ by se lišila vlastně jen v n.add(o) na místě [n add:o] z Objective C, a v tom, že v C++ universální typ id neexistuje, takže tam musíme použít Real*. Tím se ale funkce stala nepoužitelnou pro cokoli, co není typu Real* (nebo alespoň -- v případě, že programátor nezapomněl explicitně označit metodu add jako virtual -- ukazatel na dědice třídy Real). Je samozřejmě pravda, že takhle jednoduchou funkci můžeme implementovat jako makro či šablonu -- pak bude vše v pořádku; princip ovšem beze změny zůstává platný i pro komplikované služby, pro které se makra či šablony nehodí...

Naproti tomu, použití jiných objektů než těch původně zamýšlených v dynamickém systému

...v místech, kde se očekává pouze reálné číslo, budeme moci použít i komplexní (avšak využije se pouze jeho reálná složka) ╨ to není nesmyslné;
Zde prosím o příklad: mne žádný rozumný nenapadá.

Ale jistě: dává to perfektní smysl pro všechna komplexní čísla s nulovou imaginární částí -- tedy de facto čísla reálná, jen jinak vyjádřená. Funguje to zcela korektně, bez jakýchkoli implicitních či explicitních převodů.

Virtuální a nevirtuální metody

Podívejme se na příklad s okny -- abych jej nemusel celý opakovat, je v minulém článku pana Viriuse. Prosím čtenáře, aby se podíval na celý příklad -- já z něj zde budu citovat jen některá konkrétní tvrzení, s nimiž bych rád polemizoval.

Obecné okno se vykresluje vždy jako bílá, šedě orámovaná plocha. .... Potomkem obecné třídy okno bude třída PomocneOkno, které se vykresluje jako šedá neorámovaná plocha. Vedle toho PomocneOkno nabízí řadu dalších služeb...

Především, struktura služeb je zřetelně špatná: třída PomocneOkno nabízí nové služby (proč ne), ale přitom zakrývá implementaci služeb dosud existujících, jež stále mají smysl (tj. zobrazení orámovaného okna). Nejenže to je špatný návrh; je to navíc typ chyby, který by se opravdu vyskytovat neměl: jaký by to vůbec mohlo mít smysl?

Skutečně, představte si, že jste na místě programátora třídy PomocneOkno, a implementujete možnost spolupráce s myší a řadu dalších služeb... a přitom odstraníte možnost vykreslit rámeček?!? Nemohu si pomoci, ale ať o tom přemýšlím z které chci strany, napadají mne jen dvě možnosti:

...Jenže editační pole chceme nakreslit stejně jako obecné okno, tedy bíle s šedým orámováním.

Nu, v prvém případě prostě použijeme metodu zobrazSRameckem . Ve druhém případě je naopak dobře, že původní metodu nemůžeme volat, protože by šlo o hrubé porušení principu zapouzdření -- s nežádoucími důsledky: rámeček by se vykreslil naprosto nesmyslným způsobem (protože původní metoda nepočítala s transformacemi souřadnic, a proto byla překryta).

Samozřejmě, existuje ještě třetí možnost: původní programátor třeba nejprve chtěl ve třídě PomocneOkno implementovat transformace souřadnic, ale nezbyl mu na to čas... proto je metoda zobraz překryta, ale vlastně k tomu není důvod. Co teď? Zdálo by se, že přeci jen nastává chvíle, kdy by se hodilo vyvolat tu "původní" implementaci?

Nezlobte se, ale jako člen i vedoucí týmů, které pracovaly na rozsáhlých komerčních systémech, u nichž zákazníkům záleží na spolehlivosti, zdůrazňuji: ne, stokrát a tisíckrát ne!

Důvod je prostý: stačí si uvědomit, že v reálném systému píšeme aplikace, jež mají korektně fungovat i za pět nebo deset let, kdy bude daná knihovna již dávno k dispozici v nové versi! A v této nové versi může být transformace souřadnic do třídy PomocneOkno doplněna. Kód, který korektně dodržuje zapouzdření, bude pracovat i nadále bez problémů; kód, který volá nějaké Okno::zobraz, bude rázem po upgrade knihoven kreslit úplně nesmysly!

Lze se samozřejmě přít o tom, zda takovéto řešení porušuje nebo neporušuje zapouzdření. Domnívám se, že ne.

Vlastně se už budu opakovat -- a čtenář, který dobře pochopil předchozí text, může zbytek tohoto odstavce klidně přeskočit. Dopustím se však té redundance proto, že tohle je skutečně důležité -- programátor (či spíše designer API), který nebude mít tyto principy na paměti, nemůže vytvořit systém, ve kterém by se dalo skutečně spolehlivě, pohodlně, robustně a efektivně programovat.

Takže, řešení tohoto typu:

- Společný předek ╨ třída Okno ╨ zveřejnil, že má metodu zobraz(), která kreslí bílé, šedě orámované pole. (Nezveřejnil jen název metody, ale i výsledek své činnosti.);
- Její potomek, třída PomocneOkno, zveřejnil, má metodu zobraz(), která kreslí šedé, černě orámované pole.

skutečně hrubě a zásadně porušuje zapouzdření. Prostě proto, že zobraz je jedna metoda, která dělá jednu věc. Chceme-li dělat více různých věcí, je na místě použít více různých metod (zobrazSRameckem, zobrazBezRamecku).

Proč by si potomek třídy PomocneOkno nemohl vybrat mezi implementacemi, které nabízí bezprostřední a vzdálený předek? Ani jedno nepředpokládá znalost vnitřní struktury objektů ╨ využíváme pouze to, co o sobě zveřejňují. (Informace o struktuře dědické hierarchie patří, pokud vím, k základním informacím o každé objektové knihovně, a to nejen v C++, ale např. i v Javě.)

Protože implementace na úrovni nějaké třídy implicitně předpokládá znalost vnitřní struktury: jistěže ji nepotřebuje znát ten, kdo metodu volá -- znal ji však ten, kdo metodu napsal! Implementace na úrovni vzdáleného předka tedy odpovídá jeho vnitřní struktuře, a ta se mohla v bezprostředním předkovi změnit. Tím, že voláme metodu vzdáleného předka, tedy vlastně říkáme "zacházej s vnitřní strukturou objektu třídy PomocneOkno stejně, jako kdyby to byla vnitřní struktura objektu třídy Okno" -- a je snad na první pohled zřejmé, že to je obecně špatně. Třeba zrovna kvůli té transformaci souřadnic...

Je nevirtuální metoda totéž jako final?

nepředpokládáme to, pravda ╨ ale třeba za dva roky ta situace nastane! V Epocu, který je kompletně postaven na C++, se mi již mnohokrát stalo, že bych býval potřeboval mírně pozměnit chování některé standardní knihovní třídy... ale ouha, nešlo to.

To se může stát bohužel i v Javě. Čas od času zjistím, že bych potřeboval předefinovat metodu předka a přizpůsobit ji k obrazu svému, ale ouha ╨ její tvůrce předpokládal, že se měnit již nebude, nebo si to dokonce nepřál ╨ a deklaroval ji jako final
.

Ale to ne -- přeci je obrovský rozdíl mezi tím,  zda

Je snad zřejmé, že v důsledku takového přístupu je nedostatečně promyšlená třída v C++ obvykle neopravovatelná (protože programátor se patrně neobtěžoval psát něco navíc, tj. "virtual" tam, kde by to bylo zapotřebí), zatímco v Javě naopak budou obvykle z téže příčiny -- nepsat nic zbytečného navíc -- označeny "final" jen ty třídy, jejichž programátor pro to měl skutečný a vážný důvod (upřímně, nevím jak je to s třídou String v Javě -- sám používám nesrovnatelně lepší NSString z API Cocoa, a její dědice vytvářet mohu kdykoli se mi zachce).

Navíc, i kdyby někdo používal "final" jen tak pro legraci, v dynamickém systému skutečných objektů a neomezeného polymorfismu je dokonce i takováto situace řešitelná! Zde je totiž možná další věc, o níž ve statickém systému typu C++ nemůžeme ani snít: vkládání objektů s přesměrováním zpráv.

Princip je jednoduchý:

Pak prostě budeme instance této nové třídy používat namísto instancí třídy původní (to je druhá věc, jež by v C++ nešla, neboť vyžaduje polymorfismus nezávislý na dědické hierarchii); je snad vidět, že aniž bychom jakkoli využili dědičnost, ze všech praktických hledisek jsme přesto vytvořili dědice -- což je přesně to, co jsme potřebovali.

Co je, propána, vlastně ukazatel???

Stále jde o problém s přetypováním objektu, jenž je dědicem A i B, z A* na B*. Mám za to, že celý původní příklad již je zbytečné opakovat -- pro ty, kdo si jej nepamatují, je zde. Jde o "céčkovou" konstrukci

(A*)objekt

jež oproti tomu, co by napovídal zdravý rozum v C++ nemusí být korektní ani v případě, že objekt byl instancí obou tříd A i B.

Především by snad stálo za to si konečně vysvětlit v čem je zakopaný pes. Problém spočívá v tom, že uživatel normálního objektového jazyka, jakým je Java nebo Objective C, stejně jako obyčejný "céčkař", bude velmi pravděpodobně automaticky předpokládat, že

V obyčejném C by zřejmě právě takto objekty implementoval (a skutečně tomu tak v -- pokud vím -- jediné komerčně existující implementaci objektového systému v C, jíž byl Epoc/16 firmy Psion, bylo). Nejinak v Objective C. V Javě to vlastně nelze zjistit (protože Java nemá ukazatele), ale je dosti zřejmé, že tam tomu tak interně ve skutečnosti bude také.

V C++ tomu tak ale není!

Z toho vyplývají všechny problémy, speciálně z druhé části negace -- v C++ obecně neplatí, že by ukazatel na objekt byl ukazatelem na začátek struktury, jež jej v paměti representuje! Speciálně, dva různé ukazatele na tentýž objekt mohou obsahovat různé adresy. Pak je zřejmé, že a proč musí být s přetypováním v C++ problém. V C (nebo v Objective C, a vlastně i v Javě, nakolik to v ní lze posoudit) je např. samozřejmé, že výraz

(unsigned)(typ_A*)ukazatel==(unsigned)(typ_B*)ukazatel

je vždy pravdivý. V C++ tomu tak není -- pokud typ_A a typ_B jsou třídy, a ukazatel obsahuje ukazatel na objekt, který je jejich společným dědicem, výraz bude naopak vždy nepravdivý.

S tím nemá dynamic_cast vcelku nic společného. Ten pouze (je-li vůbec k dispozici, a použijeme-li patřičný přepínač při překladu -- viz diskuse problémů, spojených s tímto operátorem v mém minulém článku) zajistí, aby toto prapodivné přetypování, jehož sideefektem je změna hodnoty ukazatele, fungovalo za všech okolností "korektně" (tj. aby se hodnota ukazatele změnila z hlediska C++ správným způsobem). Použijeme-li normální, "céčkové" přetypování, na principu se nic nezmění, jen to bude fungovat korektně jenom někdy (totiž v případech, kdy překladač přesně zná vzájemné vztahy obou tříd).

Problém je, že tvůrci Javy převzali sice syntax jazyků C a C++, ale dali jí nový význam. Proto může totéž přetypování v Javě opravdu vyjadřovat přání uvedené v citátu. Jenže chceme-li něco podobného říci v C++, musíme použít prostředky C++ a nenechat se zmást podobností s Javou.

Ale proboha, vždyť to je přesně naopak! Syntaxe jazyka C je zde převzata (přinejmenším) do tří více či méně objektových jazyků -- Objective C, C++ a Javy. Jediný z nich opravdu mění sémantiku operátoru přetypování natolik, že někdy funguje podivně a někdy vůbec -- a to je právě C++! Jak v Javě, tak i v Objective C se přetypování ukazatelů (v Javě referencí) chová stejně jako v obyčejném C.

To není chyba C++, ale programátora.

S tím ovšem nelze než souhlasit -- skutečně je tomu tak. Chyba C++ spočívá v tom, že takovou chybu umožňuje -- chybu tím horší, že nastává nejen při "céčkovém" přetypovávání (jemuž se, koneckonců, vždy můžeme vyhnout), ale i při dynamickém spojování modulů, a tam již je problém opravdu značný, a jednoduché řešení (jiné než přejít na Javu či Objective C) nemá -- podrobnosti jsem opět popsal minule.

Pár poznámek k operátoru dynamic_cast

Netuším, proč překladač pana Čady neobsahuje tento operátor...

Nu, já také ne. Samozřejmě, pokud by šlo o můj překladač, mohli bychom tím celou věc uzavřít -- je to můj problém, a je zbytečné jej řešit.

Nejde však o můj překladač, ale o standardní a (dokud X.soft nedokončí své XSdk) také jediný překladač pro Epoc. Protože všude jinde mohu přesnadno programovat v rozumných jazycích, je mi celkem jedno, mám-li např. ve windows takový operátor k dispozici -- stejně tam budu psát aplikace ve stabilním, robustním a spolehlivém Objective C v OpenStepu for Windows (navíc tím získám i portabilitu). Naopak, programuji-li pro Epoc, v současnosti musím tento překladač používat. Navíc, je-li pravda to, čeho se obávám -- totiž že knihovny, přeložené s přepínačem -rtti, budou nekompatibilní s knihovnami přeloženými bez něj -- bude operátor dynamic_cast v praxi dokonale k ničemu i v případě, že příští verse překladače jej bude nabízet.

Z toho důvodu se přiznám, že je mi poměrně jedno proč tomu tak je -- podstatné je že tomu tak je.

Programovací jazyky se vyvíjejí, a to rychleji, než je nám programátorům milé, a nezbývá, než to vzít na vědomí.

Inu, proč ne -- já jsem jen rád, že dokonce i svět uživatelů C++ postupně chápe, co je k programování zapotřebí, a objevují se v něm služby, jež tak či onak simulují rozumné chování plně dynamických objektů. Třeba za dalších pět let bude C++ obsahovat i selektory, takže bude možné spojovat moduly psané v C++ s moduly z jiných objektových jazyků, a za dalších pět zaniknou potvornosti typu nevirtuálních funkcí, objeví se protokoly a standardní kořenová třída (nebo protokol) se základními službami typu isKindOfClass nebo respondsToSelector (jež dynamicky ověřuje je-li objekt schopen zpracovat zadanou zprávu)... Pak již bude rozdíl mezi C++ a Objective C skutečně jen v syntaxi, a asi i já rád přejdu na C++ pro některé jeho velmi pohodlně služby (jako je třeba možnost deklarovat proměnné skoro kdekoli, s rozumem využívaný overloading operátorů, nebo implicitní hodnoty argumentů). Těším se na to.

Prozatím však potřebuji jazyk, ve kterém jsem mohl pro zákazníka napsat kvalitní aplikaci v roce 1993, a ve kterém ji dodnes mohu udržovat; jazyk, který jsem před šesti lety použil pro napsání knihovny, již používám dodnes nejen pro aplikace, psané nad nejnovějším překladačem téhož jazyka, ale i pro jiné aplikace psané ve fungl nové Javě -- o jejíž existenci se v době, kdy jsem knihovnu psal, ještě nikomu ani nezdálo; přesto jsou služby vzájemně použitelné! Takovým jazykem C++ docela určitě není. Možná jím bude za deset let; na to však čekat nemohu...

Samozřejmě, že někteří programátoři takové požadavky nemají a nikdy mít nebudou. Píši však tyto řádky proto, aby ti, kdo si dokáží věci rozmyslet a podobné požadavky mají nebo mít mohou, nezačali z neznalosti používat naprosto nevhodné C++, a pak -- v době, kdy v něm již budou mít hotové statisíce řádků firemních knihoven -- teprve nenarazili na jeho problémy, a nezačali za původní nešťastné rozhodnutí draze platit. Podobně, jako dnes draze platí (nejen) můj tým za nesmyslné rozhodnutí firmy Psion používat C++ jako základní jazyk systému Epoc/32.

Jsem přetížen overloadingem!

Nu, nezlobte se, že celkem nepodstatnou drobnost tak rozebírám; naše krásná řeč však je můj oblíbený koníček -- odpusťte tedy, prosím, budu-li jej trochu rajtovat... Snad odjakživa byli v zásadě lidé dvou názorů na tuto věc. Jedni křičeli "Chraňte češtinu před cizími vlivy!"; ti nám dali magnetoskop, čistonosoplenu a klapkobřinkostroj. Druzí dobře věděli, že Naše Paní cizí slova dokáže snadno přijmout a přetavit ve svá; od těch máme rytíře, lampu, svetr, tramvaj, tenis, dikobraze nebo bajty -- oproti těm prvým, jimž počítačové slovo skládalo se (tehdy) ze dvou buněk. Netřeba jistě hádat, do které skupiny se řadím -- stejně jako třeba pan Pavel Eisner...

Trochu konkrétněji, "kalk" v tomto kontextu neznamená pouze překlad, ale nešikovný, neobratný, příliš doslovný překlad. Tak tomu je i s "přetížením"; jeho autor si patrně tento termín našel ve slovníku, avšak -- stejně jako tolik dalších nedouků, již dnes bohužel překládají z angličtiny odborné texty i beletrii -- nepochopil, že snad neexistuje slovo, jež by se dalo překládat bez ohledu na kontext.

Nejinak i "load": to slovo sice může znamenat "náklad, zátěž", avšak také "umístění, zavedení, nabití". "Overload" pak je sice bezpochyby "přetížení" v názvu skvělé knihy pana Haileyho, u operátorů v programovacích jazycích však jde spíše o "převedení" (operátoru na jiný význam) či "změnu umístění" (na operátor je "přemístěna" jiná služba), případně "přebití" (operátoru jiným obsahem).

Málo platné, co se dá čekat v nešťastné zemi, ve které se může v televizním filmu pojem "general attorney" překládat jako "generál Atorný", a kde může vyjít výpravná, drahá a vázaná kniha, v níž je "Two big brass ones, that's what he's got hangin'" přeloženo slovy "Dvě velké mosazné, to mu připíchli"...

A ještě k "přetížení" (a "vláknům" /thread/, "datovodům" /pipe/, a mnoha dalším zpotvořeninám): programování -- na rozdíl od obecného užívání počítačů -- bylo, je a bude anglofonní činností. Dokonce ani blázniví Francouzi se nepokusili změnit klíčová slova if/then/else na jejich francouzské ekvivalenty; to jen v Československu skutečně vznikla jakási varianta Pascalu, v níž se psalo ak/tak/inak! Moji programátoři píší většinou i poznámky ve zdrojových textech anglicky -- a dobře vědí proč.

A na závěr...

Na závěr si dovolím parafrázovat výrok, který svého času řekl W. Churchill o něčem úplně jiném: C++ je nejhorší myslitelný jazyk. Bohužel, všechny ostatní jsou ještě horší

Opravdu, co dodat? Maně mne napadá, že jiný moudrý pán pravil timeo hominem lectorem unius libri.

Copyright © O.Čada <ocs@ocs.cz>, Chip 2000