Káva? Nebo C++? Jak je libo...

Miroslav Virius

Článek pana Čady „Nad kávou bychom se nakonec shodli, ale já mám raději kakao...“ představuje hozenou rukavici; mně nezbývá, než ji zvednout. Ostatně diskuse o vztahu C++ a Javy – a programovacích jazyků vůbec – bude jistě zajímavá nejen pro nás dva.

V některých místech budu citovat úryvky z článku pana Čady, ke kterým se chci vyjádřit. Citace budou sázeny kurzívou a odděleny od ostatního textu mezerou. Budou samozřejmě vytržené z kontextu; ovšem článek pana Čady najdete na tomto CD zároveň s mým, takže pro vás nebude problém si odpovídající místo najít a přesvědčit se, zda – a na kolik – jsou mé argumenty k věci.

C, C++, Java a učení

Začnu něčím, o čem jsme dosud nehovořili, ale 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++. Neobjektově, samozřejmě, ale objektový přístup se může učit průběžně. K seznámení s tím, co z jazyka C v C++ chybí nebo co může mít jiný význam, postačí přečíst si pětistránkový článek – a to je záležitost na 20 minut i s důkladným přemýšlením, o co jde. Velice podstatné také je, že převážnou většinu knihoven jazyka C lze používat bez problémů v i C++. Vše ostatní, co C++ nabízí, lze zvládat už při práci.

Z toho „všeho ostatního“ je samozřejmě nejdůležitější objektový přístup. Zvládnutí základních syntaktických a jiných pravidel pro objekty v C++ není příliš náročné, daleko horší je naučit se objektově myslet, objektově programovat. Zvládnout objektovou analýzu a objektový návrh programu; a k tomu je třeba čas, kterého programátor – pokud ho programování opravdu živí – nemá nikdy dost.

V C++ lze programovat na jedné straně zcela neobjektově, na druhé straně prakticky čistě objektově, a mezi těmito dvěma styly je možný téměř plynulý přechod. Proces učení C++ lze popsat zdánlivě paradoxní větou: „Céčkař se C++ naučí za týden, ale učit se ho bude půl roku.“ V onom týdnu je i aktivní zvládnutí šablon, prostorů jmen, výjimek, dynamické identifikace typů a dalších nástrojů, které v jazyce C nebyly.

Jestliže posadíme céčkaře k Javě, bude mu adaptace trvat přece jen o něco déle. 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í. 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.

Je ovšem pochopitelné, že naprosto jinak se bude na Javu a C++ dívat programátor, který zná nějaký objektový jazyk, jako je třeba Smalltalk. Jemu bude javský přístup známý, nanejvýš mu bude v některých ohledech připadat zbytečně omezený.

Na závěr tohoto odstavce si dovolím poznamenat, že samozřejmě i v Javě lze programovat neobjektově: Vytvoříme metodu main a další metody jako procedury svého programu a to vše z povinnosti uzavřeme do nějaké třídy. Výsledkem bude ryze procedurální program, i když v téměř čistě objektovém jazyku Java.

Některé učebnice Javy takhle začínají. I když se v této souvislosti vtírá vzpomínka na článek o skutečných programátorech a pojídačích koláčů, který tvrdil, že „skutečný programátor dokáže napsat fortranský program v jakémkoli jazyce“, nejde rozhodně o projev profesionality – spíš je to projev nepochopení Javy a její filozofie.

Přetěžování

Přetěžování (či chcete-li overloading – ale já dávám přednost českým termínům, protože píšu převážně pro české čtenáře) opravdu není pro diskusi o objektově orientovaném programování rozhodující. Nicméně naše povídání je o především Javě a C++, a proto sem patří.

Přetěžování operátorů je nástroj, který bývá v kontextu jazyka C++ s objektovým programováním spojován, ale rozhlédneme-li se po okolí, zjistíme, že ho poskytují i neobjektové jazyky, jako je třeba Fortran 90.

Java přetěžování operátorů ve skutečnosti obsahuje. Zamyslíme-li se nad výrazem

a + b,

zjistíme, že jeho význam se liší podle toho, zda jde o sčítání dvou čísel typu int, dvou čísel typu double nebo o spojování dvou řetězců. Máme tedy stejným symbolem označeno několik různých operací a překladač podle typu operandů určuje, který z operátorů + použije. 

To znamená, že překladač Javy se s přetěžováním operátorů umí vyrovnat, nedovoluje to však obyčejným programátorům, ale jen tvůrcům jazyka. V tom je C++ vůči programátorovi daleko poctivější.

Kdybychom změnili význam třeba operátoru přiřazení pro některý objektový typ (třídu), máme problém – s objekty toho typu pak prakticky nebude možné vůbec pracovat, protože nebude možnost nijak nastavit hodnotu odpovídající proměnné!

Vzhledem k tomu, že Java pracuje s objekty pouze prostřednictvím referencí, by opravdu nebylo rozumné umožnit v ní přetěžování přiřazovacího operátoru. Podobně by asi nebylo rozumné dovolit přetěžování operátoru tečka a ještě několika dalších.

Jenže to stále není důvod, proč přetěžování jako takové nepodporovat. Koneckonců, C++ také neumožňuje přetěžovat všechny operátory – mezi výjimky patří např. už zmíněná tečka, používaná podobně jako v Javě pro přístup k složkám instancí.

...přece jen je syntaktická analýza zdrojového textu bez možnosti overloadovaných operátorů snazší; ještě větší význam to má u interpretovaných jazyků – jako je (většinou) právě Java.

Začnu trochu obecněji: Často se setkávám s výhradami, že přetěžování operátorů je neefektivní nebo že způsobuje složitost překladače.

První výhrada je nesmyslná: jsou-li a a b dvě instance objektového typu, pro který je přetížen operátor +, znamená zápis

 

a + b

 

totéž co

 

operator+(a, b)

 

tedy volání funkce (metody).

Druhá výhrada je oprávněná, alespoň částečně: Může-li programátor přetěžovat operátory, musí s tím překladač počítat a v případě potřeby umět podle jistých pravidel rozhodnout, který operátor se má použít, tj. kterou metodu má zavolat. Není to ovšem o mnoho složitější než rozlišování přetížených metod nebo vestavěných operátorů, a to Java umí.

Skutečnost, že Java je interpretovaný jazyk, s tím nemá nic společného: Víme přece, že zdrojový text se nejprve překládá do bajtového kódu, a teprve ten se interpretuje. Nepochybuji o tom, že rozlišování přetížených metod v Javě řeší už překladač, nikoli interpret JVM, a není důvodu, proč by tomu tak nemohlo být i v případě operátorů. Na složitosti bajtového kódu, stejně jako na složitosti JVM, se tím nemusí nic změnit.

A navíc, jak často v dnešním téměř bez výjimky GUI světě opravdu využijeme standardní výstup? Jen výjimečně; nesrovnatelně častěji pracujeme se stringy, pro které nám overloading operátoru << je málo platný...

Pomiňme skutečnost, že standardní výstup se používá např. při programování CGI skriptů, při ladění a v mnoha jiných situacích – ono to opravdu není mnoho ve srovnání se záplavou graficky orientovaných aplikací. Jenže objektové datové proudy jazyka C++ se v žádném případě neomezují pouze na standardní vstupy a výstupy.

Přetížené operátory >> a << se kromě standardních vstupů a výstupů a práce se soubory dají použít – a také hojně používají – pro práci se znakovými řetězci. Tyto „paměťové“ proudy umožňují formátovat data a výsledek zapisovat do znakových polí. Stejně dobře umožňují číst data ze znakových řetězců naprosto stejným způsobem jako ze souboru. Můžeme se na ně dívat jako na analogii funkcí sprintf() a sscanf() z jazyka C.

Existují dokonce ve dvou implementacích: Standardní třídy stringstream, istringstream a ostringstream jsou definovány v hlavičkovém souboru <sstream>, ve starších implementacích jazyka C++ (a kvůli zpětné kompatibilitě také ve většině novějších) najdeme proudy strstream, istrstream a ostrstream, které jsou definovány v hlavičkovém souboru <strstrea.h>.

 A právě zde poskytují přetížené operátory >> a << pohodlí, které mi v Javě chybí. Navíc, vzhledem k objektové povaze datových proudů umožňují tyto operátory vstup a výstup libovolných datových typů, i typů, které v době návrhu ještě neznáme.

Přetěžování a polymorfismus

Za další, s overloadingem je problém i vinou polymorfismu. V plně objektových jazycích dává velmi dobrý smysl třeba používat proměnné pro obecné, beztypové objekty (id v Objective C, Object v Javě; v C++ to není možné, ale teoretickým ekvivalentem by mohl být void*). Overloading by proto musel být nikoli vlastností překladače (jako je tomu v C++), ale runtime!

No a?

Pokusme se ale o seriózní odpověď. V tomto odstavci jde o dvě věci: Za prvé o beztypové kontejnery a za druhé o možnost přetěžování operátorů pro beztypové instance.

Začneme druhou z nich, neboť bezprostředně navazuje na předchozí diskusi. Polymorfní chování objektů bývá implementováno jako běhová záležitost, nejinak je tomu i v Javě nebo v C++. Protože však použití přetíženého operátoru není nic jiného než volání metody, mohli bychom tvrdit, že nejde o žádný zvláštní problém.

Jenže on tu problém je, a ne malý: 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 polymorfismus 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.

To ovšem neposkytuje Java ani C++.

Na druhé straně skutečnost, že Java neposkytuje přetížené operátory, problém vícenásobného polymorfismu nikterak neřeší. Jestliže potřebuji provádět nějaké binární operace nad prvky beztypového kontejneru, pak je jedno, zda tyto operace zapíšu pomocí operátoru nebo jako volání metody – vícenásobný polymorfismus si musím naprogramovat sám.

Pokud jde o samotné beztypové kontejnery, je celá záležitost také trochu složitější, než by mohlo vyplývat z předchozího citátu.

Především, v čistě objektových jazycích je obvyklé, že všechny objekty jsou navzájem příbuzné – mají společného předka, třídu, která se obvykle jmenuje Object a která implementuje společné chování všech objektů. „Beztypový“ kontejner obsahuje odkazy (reference) na instance typu Object, a protože v objektovém programování může potomek vždy zastoupit předka, lze do takového kontejneru ukládat opravdu téměř cokoli – přesněji jakékoli objekty, neboť ty jsou instancemi potomků třídy Object. (To je, alespoň v případě Javy, dost podstatné; do „beztypového“ kontejneru v Javě nemůžeme ukládat hodnoty primitivních typů, např. čísel, pokud je „neobalíme“ do instancí vhodných tříd.)

„Beztypovost“ tedy je něco, co v C++ nemá přesnou analogii; třídy mohou být na sobě nezávislé, mohou být členy mnoha různých dědických hierarchií.

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í.

V C++ můžeme samozřejmě použít kontejner založený na beztypových ukazatelích (void*); není to ale nejlepší řešení, protože do takového kontejneru bychom mohli uložit opravdu cokoli, číslo bot vedle ptakoještěra, a tím bychom se připravili jednak o možnost typové kontroly už při překladu a za druhé o polymorfismus. (Museli bychom si spolu s každým objektem ukládat informaci o jeho typu a podle toho se pak orientovat při jeho zpracovávání.)

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, který se může jmenovat třeba také Object, a použít kontejner, který může obsahovat odkazy na instance této třídy.

Pokud od takovéhoto společného předka odvodíme jak ptakoještěry, tak okna, budeme je tam smět ukládat vedle sebe, pokud ne, budeme muset mít zvláštní kontejner na okna a zvláštní kontejner na ptakoještěry; obojí může mít svou logiku – záleží na tom, co vlastně potřebujeme.

Ostatně řešení založená na objektových hierarchiích se společným předkem byla obvyklou součástí různých knihoven, dodávaných s překladači C++ na počátku devadesátých let. V současné době se ale zpravidla používají kontejnery založené na šablonách. Jejich výhodou je, že umožňují stejně snadno pracovat s objektovými i s neobjektovými typy a přitom v nich lze snadno omezit typ ukládaných hodnot. Navíc jsou součástí standardní šablonové knihovny jazyka C++.

Komplexní a reálná čísla

Přiznám se, že poznámku o nevhodnosti odvozování komplexních čísel jako potomka čísel reálných jsem napsal jen jaksi na okraj, jako něco, co do ostatního textu vlastně nezapadalo a čemu jsem nepřikládal valnou důležitost. Nicméně:

Asi každý programátor potvrdí, že běžná – a rád bych zdůraznil, že naprosto rozumná! – praxe vede přesně opačným směrem: nejprve implementuji ten nejjednodušší případ a vše odladím. Pak přidám nějaké rozšířené služby, a zase vše odladím...

To zní krásně, ale pokud si něco podobného vyzkoušíte v případě komplexních čísel a použijete Javu nebo C++, nejspíš se dostanete do problémů. Množina reálných čísel je prostě podmnožinou čísel komplexních, a proto je opačná hierarchie doslova postavená na hlavu.

Na druhé straně mi nezbývá, než souhlasit, že po stránce implementace to svádí k uvedenému postupu: Nejprve odladíme reálná čísla, pak přidáme imaginární složku...

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ží. Postup, který uvádí pan Čada, rozhodně má jasnou logiku, ale vede v určitých situacích k nesmyslného chování programu.

Takže obě cestu se z určitého úhlu sice třpytí, ale zlatá není ani jedna.

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. Tak otevřeme cestu k automatickým konverzím reálných čísel na čísla komplexní a nahradíme dědičnost. Nyní budeme moci na místě komplexního čísla bez problémů použít číslo reálné, zatímco na místě reálného čísla číslo komplexní zapsat nepůjde – tak, jak je to v matematice obvyklé.

...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á.

Zjišťování typu instance za běhu

Musím se upřímně přiznat, že neznám způsob, jak z této pasti vyklouznout v rigidním jazyku typu C++, kde polymorfismus ani triky typu isKindOfClass nejsou k dispozici.

Ale jsou. Polymorfismus je sice omezen na jednotlivé dědické hierarchie, ale to je při šíři možností, které tento jazyk poskytuje, přirozené – ostatně v Javě je tomu také tak, neboť tam všechny objekty tvoří jedinou hierarchii. (Ani Java není plně polymorfní: zkuste poslat nějakou zprávu třeba celému číslu, nezapouzdřenému do žádné obalové třídy.)

A pokud jde o zjišťování typu instance za běhu programu (něco jako isKindOfClass), k tomu slouží v C++ standardní operátor typeid, který umožňuje dynamickou identifikaci typů. I tady – podobně jako u operátoru dynamic_cast – narazíme na omezení: aby fungoval opravdu „dynamicky“, musí pracovat s polymorfními třídami. Ovšem to je v případě jakékoli dědické hierarchie víceméně samozřejmost.

Vrátíme-li se k předchozímu příkladu, mohli bychom tedy i v C++ postupovat tak, že definujeme nejprve třídu Real a od ní odvodíme jako potomka třídu Complex. Na místě, kde bychom chtěli pouze reálná a nikoli komplexní čísla, bychom zkontrolovali typ předaného parametru a v případě nesouladu vyvolali např. výjimku.

Jenže ... nemohu se ubránit dojmu, že to je stejně krkolomné řešení jako odvozovat reálná čísla od komplexních a tahat s sebou pořád nulovou imaginární částí.

Dědičnost?

Obávám se, že příklad komplexních a reálných čísel ukazuje, že současné pojetí dědičnosti tak, jak je přináší Java nebo C++, je v některých situacích nevhodné. Reálné číslo je zvláštním případem komplexního čísla, ale takovým, kde má jedna složka nulovou hodnotu, a proto bychom ji potřebovali v odvozené třídě vypustit. To je něco, co Java ani C++ neumí – při dědičnosti v jejich pojetí lze datové složky přidávat, nikoli ubírat.

Přitom nejde o případ nijak ojedinělý: bod ve dvourozměrném prostoru lze považovat za speciální případ bodu ve třírozměrném prostoru, ve kterém je jedna složka nulová, hada lze považovat za zvíře bez nohou atd. (Málokdo bude programovat obecné zvíře jako hada s nohama.)

Poznámka

Myslím, že problém s reálnými a komplexními čísly je také odrazem skutečnosti, že reálná čísla vlastně nejsou zvláštním případem komplexních čísel – alespoň ne v naprosto rigirózním matematickém pojetí.

Množina komplexních čísel je množinou uspořádaných dvojic reálných čísel. Samotné reálné číslo v této množině ležet nemůže – jedno číslo není uspořádaná dvojice čísel. Z praktického hlediska je ovšem výhodné prohlásit, že reálná čísla jsou prostě komplexní čísla, která mají imaginární složku nulovou, a proto ji nepíšeme. Toto zjednodušení je natolik výhodné, že se o něm v běžných kurzech matematiky ani nemluví, předkládá se jako samozřejmost – a v běžných matematických aplikacích to funguje.

Při objektovém návrhu to ale může způsobit potíže.

Virtuální a nevirtuální metody

Jazyk C++ nabízí mimo jiné možnost volat metodu „s plnou kvalifikací“, např.

 

u -> A::f();

 

Je-li f() virtuální metoda, potlačíme tím pozdní vazbu. Zde totiž říkáme: Bez ohledu na to, jakého typu je instance, na kterou ukazuje u, chci metodu, která je definována ve třídě A. (Samozřejmě za předpokladu, že u je ukazatel na potomka třídy A.)

To nevypadá příliš objektově, že? Přesto se to může hodit.

Objektové hierarchie nemusí být navrženy vždy optimálně; předchozí diskuse o reálných a komplexních číslech to ukázala dost jasně – a to šlo o pouhé dvě třídy, ve skutečných objektových knihovnách jich mohou být stovky. Vedle toho se může stát, že se pokusíme použít objektovou knihovnu k účelu, který neodpovídá přesně jejímu původnímu účelu – i to je poměrně běžná situace.

Už to samo je důvod, proč může být rozumné podobnou možnost nabídnout. Podívejme se ale na příklad, který by mohl pocházet z libovolného grafického rozhraní.

·      Představte si, že máme třídu Okno, která nabízí službu zobraz(). 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, které v obecném okně nejsou – například změnu způsobu zobrazení při stisknutí tlačítka myši.

·      Od této třídy bude odvozena celá řada ovládacích prvků GUI, jako jsou tlačítka, zaškrtávací políčka ap. Mezi nimi bude i editační pole – třída EditOkno – pro zadávání vstupu. Jenže editační pole chceme nakreslit stejně jako obecné okno, tedy bíle s šedým orámováním.

Zde určitě oceníme možnost implementovat metodu zobraz() pomocí metody vzdáleného předka. Jinak bychom měli následující možnosti:

n    Mohli bychom třídu EditOkno definovat jako přímého potomka třídy Okno. Pak bychom sice zdědili potřebnou implementaci metody zobraz(), ale ztratili bychom všechny speciální vlastnosti, definované ve třídě PomocneOkno (a museli bychom je v ní programovat znovu). Navíc bychom tím třídě EditOkno dali zvláštní postavení v rámci celé hierarchie, a to také není dobré.

n    Mohli bychom také třídu EditOkno definovat jako potomka třídy PomocneOkno. Pak bychom ale museli předefinovat její metodu zobraz() – a přitom psát znovu totéž co ve třídě Okno.

V obou případech by to znamenalo, že musíme určité věci programovat dvakrát, a to nikdy není dobré. (Můžete namítnout, že různé způsoby kreslení lze naprogramovat jako jednu metodu, které předáme barvy plochy a rámečku prostě jako parametry. Složitější příklad by ale zabral příliš mnoho místa.)

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.

n    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.)

n    Její potomek, třída PomocneOkno, zveřejnil, má metodu zobraz(), která kreslí šedé, černě orámované pole.

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ě.)

Je možné samozřejmě namítnout, že lepší by bylo vytvořit pomocnou třídu, kterou použijeme jak v obecném okně, tak v editačním poli. Jistě; ale co když dostanu třídy Okno a PomocneOkno již hotové? To je přece naprosto běžná situace.

Čistě virtuální metoda s implementací je věc, která se asi příliš nepoužívá. Ve skutečnosti řada i špičkových programátorů používajících C++ o ní nejspíš ani neví, ale součástí jazyka je, a proto jsem ji uvedl v přehledu možností, které C++ nabízí. Nejde o nic více, než o možnost, jak potomkům nabídnout možnou implementaci a přitom tuto metodu deklarovat jako čistě virtuální (či chcete-li abstraktní).

C++ nabízí vedle sebe jak virtuální, tak i nevirtuální metody. Lze se nad tím pozastavovat, lze s tím nesouhlasit, ale to je asi tak vše, co s tím lze dělat – nějak podobně to řekl Jára Cimrman o něčem úplně jiném. Jestliže o nějaké třídě nepředpokládám, že by se mohla stát předkem jiné třídy, použiji v ní nevirtuální metody.

Nu, v tom je právě ta chyba: 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. Nebo tak deklaroval celou knihovní třídu – například String. (Nechápu, proč si od ní nesmím odvodit vlastního potomka, který by lépe vyhovoval mým záměrům, ale je to tak.)

V obou jazycích, a nejen v nich, se může stát, že se představy tvůrce knihovny a programátora, který ji používá, rozejdou. Bohužel je to vždy programátor, který pak musí psát, psát a psát...

Co je vlastně třída a co objekt?

V článku „Přátelské nedorozumění nad kávou“ jsem napsal

Přetypování (Object*)o překladači vlastně říká: „Zde máš ukazatel o na třídu Interface; buď tak laskav a zacházej s ním jako s ukazatelem na třídu Object.“

Jazyk C++ něco jako „ukazatel na třídu“ ve skutečnosti nezná, a proto si mnoho programátorů zkracuje dlouhý termín a místo „ukazatel na instanci třídy Object“ říká prostě „ukazatel na třídu Object“ nebo „ukazatel na Object“. V diskusi o samotném C++ by to nevadilo, ale my se bavíme také o jazycích, kde ukazatele na třídy skutečně existují a to mohlo způsobit nedorozumění. Pokud k nějakému došlo, omlouvám se.

Vraťme se ale k významu uvedeného přetypování. Měli jsme dvě nezávislé třídy, Object a Interface, a ukazatel na jejich společného potomka. Pro lepší orientaci si to zopakujeme:

 

class Interface {public: // První nezávislá třída

   virtual void metoda()=0;

};

 

class Object { public: // Druhá nezávislá třída

  virtual void xxx() {

     printf("Metoda vsech objektu...\n");

  }

};

 

class Xxx: public Object, public Interface {

 public: // Společný potomek

  virtual void metoda() {

    printf("metoda tridy Xxx\n");

  };

};

 

void use(Interface* o)

{ // Původní implementace z článku „Silná káva“

 ((Object*)o)->xxx(); // !

 o->metoda();

}

 

Přetypování, o které nám jde, je vyznačeno tučně a vykřičníkem v komentáři.

Problém je, že něco takového bude v Javě fungovat, ale v C++ ne. To není chyba C++, ale programátora. V C++ toto přetypování opravdu znamená právě to, co jsem chtěl říci už v článku Přátelské nedorozumění – uvedu poopravené znění svého tvrzení:

„Zde máš ukazatel na instanci třídy Interface.  Buď tak laskav a zacházej s ním jako s ukazatelem na instanci třídy Object.“

Nic více a nic méně.

Operátor (typ) nevyužívá dynamickou identifikaci typů a tak nemůže zjistit, zda je oprávněné přání

"máme ukazatel na objekt, který je instancí dvou tříd zároveň – třídy Interface a třídy Object. Až dosud jsme s ním pracovali jako s instancí Interface, nadále s ním chceme pracovat jako s instancí Object"

a pokud ano, kde by měl zděděné složky Object vlastně hledat. Takže výše uvedený citát je za dané situace v C++ opravdu jen zbožné přání programátora, který se nechal zmást podobností s Javou.

Lze se ptát, proč je to právě tak. Odpověď je jednoduchá – jde o dědictví jazyka C. Takováto interpretace odpovídá logice přetypování ukazatele typu int* na ukazatel typu void* nebo naopak, přetypování ukazatele typu int* na ukazatel typu unsigned* ap. Operátor (typ) v C++ prostě nevyužívá dynamickou identifikaci typů, s tím se musíme smířit, a tam, kde ji potřebujeme, použít operátor dynamic_cast.

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.

Jak je to s operátorem dynamic_cast?

Už minule jsem upozorňoval, že místo (typ) musíme použít operátor dynamic_cast. Přetypování a dynamické identifikaci typů v C++ jsem věnoval samostatný článek, který vyšel v Chipu 10/00 a 11/00, a proto zde pouze odpovím na připomínky z článku pana Čady.

...jak "mladý" je tento operátor v C++? Já se C++ učil zrovna z knihy pana Viriuse "Programovací jazyky C/C++" z roku 1992; možná jsem špatně četl, ale s tímto operátorem jsem se v ní nesetkal? Kromě toho, příklady zkouším v GNU C verse 2.7.2.1 v Mac OS X Server – není to samozřejmě nejnovější verse překladače, ale také to není nic prehistorického! A když jsem řešení s dynamic_cast chtěl vyzkoušet, se zlou jsem se potázal...

Přesnou odpověď bohužel neznám. Nicméně mohu se pokusit alespoň o přibližné vymezení doby, kdy se tento operátor objevil v návrhu standardu jazyka C++.

Knihu „Programovací jazyky C/C++“ jsem napsal na podzim roku 1991, na trhu se objevila na jaře 1992. V té době se o tomto operátoru ještě nemluvilo.

Na druhé straně v knize B. Stroustrupa a M. A. Ellisové „The Annotated C++ Reference Manual“, vydané nakladatelstvím Addison-Wesley v lednu 1994, již najdeme popis tohoto operátoru v dodatcích, které shrnují rozhodnutí standardizační komise. (Tato kniha sloužila dlouhou dobu jako neoficiální norma C++. Poprvé vyšla, pokud vím v r. 1991.)

V té době se přetypovací operátory dynamic_cast, static_cast, reinterpret_cast a const_cast spolu s operátorem typeid  také začaly objevovat v překladačích. Určitě je obsahoval například překladač Borland C++ 4.0, uvolněný začátkem roku 1994. Přibližně v té době se začal objevovat také v překladačích konkurenčních firem.

To znamená, že jeho specifikace je známa již alespoň sedm let a jeho implementace v překladačích je k dispozici jen o něco kratší dobu.

Netuším, proč překladač pana Čady neobsahuje tento operátor a hlavičkový soubor <typeinfo>, obávám se ale, že to není vina jazyka: Tento hlavičkový soubor je předepsán standardem (už poměrně dlouhou dobu) a proto by tam měl být – jako <typeinfo>, <typeinfo.h> nebo pod podobným jménem.

Ale na něco podobného můžeme narazit i v jiných programovacích jazycích. Jestliže se rozhodnu napsat aplet, může se mi stát, že sice na mém počítači poběží, ale webové prohlížeče většiny uživatelů budou hlásit něco jako Class not found – nebyla nalezena potřebná třída. To proto, že jsem třídu svého apletu odvodil od třídy JApplet, jak se to doporučuje v Javě 2, zatímco většina prohlížečů dosud podporuje pouze Javu 1.1, která třídu JApplet nezná.

Mohu ale spadnout i do opačné pasti: Poučen četbou učebnic zasednu k počítači a napíšu

 

class MujAplet extends JApplet {/* ... */}

 

a překladač mi vynadá, že něco takového nezná, protože mám starší verzi Javy.

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í.

Overloading a čeština

Zde si dovolím odbočit od hlavního tématu naší diskuse a ocitovat slova pana Čady z článku „Silná káva pro objektového programátora“.

...chcete-li, „přetížit“ — podle mého názoru je tento kalk z angličtiny dost zrůdný, asi jako kdybychom chtěli rozhraní říkat „meziobličej“...

Diskuse o české terminologii v programování by vydala na samostatný článek a já se k něčemu takovému už dlouhou dobu chystám, ale zde si dovolím jen několik stručných poznámek.

n    Přetěžování je doslovný překlad (kalk) anglického termínu overloading, s tím nelze nic dělat. Na druhé straně nevidím důvod, proč by se s tím něco dělat mělo: Logika původního termínu je asi tak „na toto jméno je naloženo několik významů, je tedy přetíženo“. Na anglický (přesněji americký) termín je to logické až moc – neobsahuje to narážku na Alenku v říši divů, vtip ani slovní hříčku.

n    I když to rozhodně není nejlepší, není to tak špatné a kromě toho, nic jiného se v češtině neujalo. Navíc rozhodneme-li se pro overloading, budeme muset overloadovat a říkat další příšernosti.

n    Používání původní, tedy anglické terminologie mi připadá jako nejhorší možnost vůbec. Termín by měl pokud možno vysvětlovat, a to i neznalému, a to je možné jen v případě, že použijeme češtinu. Aby bylo jasno, nebráním se používání obecně známých slov, která pocházejí z cizích jazyků a jsou blízká anglickým termínům

n    Anglická terminologie zní učeně a pomáhá „dělat z toho vědu“. To potřebují filozofové, kteří nechtějí, aby jim někdo rozuměl, protože by mohl přijít na to, že jejich řeči jsou obsahově prázdné. My si na nic podobného hrát nemusíme, programování a počítače mohou být pro řadu lidí složité, i když se budeme snažit to vysvětlit co nejsrozumitelněji, a výklad o něm rozhodně nebude obsahově prázdný, i když budeme mluvit česky.

A pokud jde o ten meziobličej: To se opravdu nepoužívá, už před dvaceti nebo více lety někdo vymyslel velice pěkný termín rozhraní, ale sám jistě víte, že meziksicht je poměrně oblíbený programátorský vtípek.

Co dodat

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ší.