Jazyk C++ KDYŽ TO CHODÍ JINAK... Zažil to asi každý programátor: někdy jeho dílko, jakkoli se v něm zdá všechno logicky i syntakticky v pořádku, při běhu produkuje zcela nečekané výsledky. Pátrání po příčinách bývá zdlouhavé a zejména začátečníkům může přinést nejednu bezesnou noc. Snad vám tedy přijde vhod upozornění na některé časté důvody podivného chování programů - zde si je předvedeme v prostředí C++, ale podobná úskalí čekají snad v každém vyšším jazyku... Vedlejší efekty Pro většinu operátorů v C++ platí, že pořadí, ve kterém se vyhodnocují jednotlivé operandy, není stanoveno. To nemá nic společného s prioritou či asociativitou operátorů. Abychom snáze pochopili, oč jde, podívejme se na jednoduchý příklad: a = f() + g() + h(); Jak známo, operátor + je asociativní zleva doprava, takže se uvedený výraz vyhodnotí, jako kdyby byl uzávorkován: a = (f() + g()) + h(); To sice znamená, že se nejprve sečtou výsledky volání f() a g() a k nim se přičte výsledek volání h(), ale už z toho nijak nevyplývá, že se bude nejprve volat funkce f(), pak g() a nakonec h(). Pořadí volání, tedy vyhodnocení operandů, je ponecháno zcela na vůli překladače; tím se mu totiž otevírají různé možnosti optimalizace. Nemilým důsledkem ovšem je, že pokud mají některé operace vedlejší efekty, může se stát, že stejný výraz přeložený různými překladači, nebo dokonce týmž překladačem na různých místech programu, dá se stejnými operandy různé výsledky. Představme si třeba, že funkce f() a g() z předchozího příkladu zvyšují o 1 hodnotu globální proměnné x a funkce h() tuto hodnotu vrací: int x = 0; int f(){++x; return 0;} int g(){++x; return 0;} int h(){return x;} Jestliže pak napíšeme x = 0; a = f() + g() + h(); bude hodnota proměnné a rovna 0, 1 nebo 2 v závislosti na pořadí, v jakém se operandy vyhodnotí. Můžete namítnout, že předchozí příklad je samoúčelně vykonstruované programátorské "zvěrstvo" a postrádá jakýkoli smysl. To je samozřejmě pravda; skutečné příklady chyb tohoto druhu bývají podstatně složitější, princip je ale podobný. Nejen funkce Vedlejší efekty funkcí jsou nejnápadnějším příkladem, do problémů se ale můžeme dostat i s operátory ++ a --. Podívejme se opět na jednoduchý příklad: int x = 5; int y = (x--)*(x--); Bude y obsahovat 20, nebo 25? Odpověď je tristní, ale už ji nepochybně uhodnete: Záleží na překladači. Například archaický, ale tu a tam stále ještě používaný překladač Borland C++ 3.1 vytvoří program, v němž bude výsledkem hodnota 20, ale novější překladače Borland C++ Builder nebo MS Visual C++ 6 vygenerují kód, který dá jako výsledek 25. (Ve všech případech bude ovšem x nakonec obsahovat hodnotu 3.) Sekvenční body V uvedeném příkladu by se mohlo zdát, že pravdu má starší překladač: nejprve vezme x - lhostejno, zda první, nebo druhé, neboť jde o touž proměnnou -, použije jeho aktuální hodnotu a pak hodnotu uloženou v této proměnné zmenší o 1. Pak udělá ještě jednou totéž. To znamená, že by měl použít jednou hodnotu 5, podruhé 4, a dostat tedy 20. Jenže nic takového není nikde předepsáno. Standard jazyka totiž pouze stanoví, že v jistých místech programu jsou definovány tzv. sekvenční body, ve kterých musí být dokončeno vyhodnocení předcházející části výpočtu včetně všech vedlejších efektů a žádný z vedlejších efektů následujících výpočtů ještě nesmí nastat. Takovým sekvenčním bodem je např. konec celého výrazu, vyhodnocení všech parametrů při volání funkce před vstupem do jejího těla, okopírování hodnoty vracené funkcí při návratu atd. V obecném případě však sekvenčním bodem není vyhodnocení součásti výrazu. Standard také výslovně uvádí, že pořadí, ve kterém nastanou vedlejší efekty, není specifikováno. Předepisuje jen, že musí nastat nejpozději při průchodu sekvenčním bodem, nic více. To znamená, že vedlejší efekty - v našem případě změny hodnot proměnných x a y - musí nastat nejpozději po vyhodnocení celého výrazu. Nikde není ale řečeno, zda např. změna hodnoty x nastane vždy po vyhodnocení uzávorkovaného podvýrazu (a dostaneme výsledek 20), nebo zda nastane dvakrát za sebou až po vyhodnocení celého výrazu (a dostaneme výsledek 25). Ani jeden z překladačů tedy svým chováním neodporuje standardu. Makra Zápis (x--)*(x--) vypadá na první pohled podivně a nepravděpodobně. Proč bychom něco takového psali? Čeho tím vlastně chceme dosáhnout? Je asi jasné, že podobné věci programátor běžně nenapíše. Přesto jejich výskyt v programu není tak nepravděpodobný, jak by se mohlo zdát; mohou totiž snadno vzniknout jako výsledek vyhodnocování maker. Stačí, když definujeme makro SQR(), které bude počítat druhou mocninu: #define SQR(x) ((x)*(x)) a později ho použijeme naprosto "logickým" způsobem int y = SQR(x--); Kdyby SQR() byla funkce, bylo by vše v pořádku - a právě podobnost použití parametrického makra s voláním funkce je častou příčinou podobných chyb. Výjimky Řekli jsme, že pro většinu operátorů není pořadí vyhodnocení operandů specifikováno. Toto pravidlo však má své výjimky, které jistě dobře znáte. Jde o operátory ||, &&, ?: a čárka. Mezi vyhodnocením jednotlivých operandů ve výrazech a||b, a&&b, a?b:c a a,b je vždy sekvenční bod. To znamená, že vždy se nejprve vyhodnotí první operand (včetně možných vedlejších efektů) a teprve pak druhý. Ovšem pozor: vztahuje se to pouze na vestavěné operátory, nikoli na přetížené operátory ||, && a čárka. (Operátor ?: nelze, jak známo, přetěžovat.) Použití přetíženého operátoru představuje volání funkce, ve kterém operandy vystupují jako parametry - a to znamená, že sekvenční bod následuje až po vyhodnocení všech parametrů. Nedefinované výrazy Z předchozího povídání plyne, že hodnota některých výrazů (nebo chování překladače při jejich vyhodnocování) není definována; takovým konstrukcím je pochopitelně třeba se v programu vyhnout. Jiné možné příklady výrazů, pro něž není chování programu definováno, jsou: i = v[i++]; i = ++i+1; Nezapomínejme, že "úplný výraz" zahrnuje i přiřazení a proměnnou na jeho levé straně a že mezi vyhodnocením podvýrazu na pravé straně a jeho přiřazením levé straně není sekvenční bod. V prvním případě tedy není jasné, zda se bude nejprve inkrementovat hodnota i a pak se i přepíše hodnotou daného prvku pole v nebo naopak. Inkrementace i pomocí operátoru ++ a změna jeho hodnoty pomocí operátoru přiřazení jsou totiž dva různé vedlejší efekty a ty mohou nastat v libovolném pořadí. Podobné je to i ve druhém příkazu. (Poznamenejme, že oba tyto příklady uvádí standard ISO 14882-1998 jazyka C++.) Na druhé straně příkaz i = i+5; je naprosto v pořádku, neboť zde nastává jediný vedlejší efekt - změna hodnoty i v důsledku přiřazení. Miroslav Virius 2/1