Jste zde

Méně známé skutečnosti o C a C++: Sekvenční body

Následující příspěvek je nejen pro ty, kterým kód a[i] = i++; připadá v pořádku, či kteří si myslí, že rozdíl mezi ++i a i++ je v tom, že ten druhý způsobí zvýšení hodnoty i až po dokončení zpracování výrazu, kde se vyskytuje ;).

Poznámka: Pro přehlednost budu mluvit především o jazyku C++ podle normy C++98; v C99 je to obdobné.

Úvod

Z optimalizačních důvodů bylo do jazyka C zavedeno pravidlo (a později převzato do C++), že pořadí vyhodnocování podvýrazů ve výrazu není mezi jednotlivými sekvenčními body (viz dále) definováno. Důsledkem je např. to, že není definováno pořadí vyhodnocení předaných argumentů při volání funkce. Člověk, který by očekával, že se budou vyhodnocovat zleva doprava, by tímto mohl být překvapen (a pokud by na tom závisel běh programu, tak by mohlo dojít k problémům).

Proč nám na pořadí vyhodnocování v C či C++ záleží? Imperativní paradigma je založeno na modifikaci stavového prostoru pomocí bočních efektů (side effects). I triviální příkaz jako i = 1; způsobuje boční efekt (hlavním cílem vyhodnocení výrazu je zisk jeho hodnoty; boční efekt je "až" druhořadý). A protože v jednom příkazu může být bočních efektů více, či výraz může obsahovat boční efekt i získání hodnoty proměnné, u které se boční efekt provádí (viz příklad pod nadpisem), je nutné znát pořadí, v jakém se boční efekty uplatní. Jen na okraj uvedu, že funkcionální jazyky (či přesněji čistě funkcionální jazyky), jako je např. Haskell, boční efekty omezují (bojím se napsat, že je vůbec neobsahují :]). Proto je v těchto jazycích větší možnost optimalizace a paralelizace výpočtu. Nechme ale úvod úvodem a přejděme k jádru věci.

Co to jsou ty sekvenční body?

Sekvenční bod je místo v programu, kde máme zaručeno, že všechny boční efekty z výpočtů, které jsou uvedeny před tímto bodem, byly provedeny (norma C++98, 19.7). Typickým sekvenčním bodem je středník -- pokud na něj narazíte, tak si můžete být jisti, že příkaz, kterým byl ukončen, byl kompletně proveden. Kromě středníku ale norma definuje i další sekvenční body (přesný výčet viz C++98 norma, sekce 1.9):

  • nepřetížené operátory čárka, &&, ||
  • ternární operátor (vyhodnocení prvního, druhého a třetího operandu)
  • na konci bloků
  • konec podmínky u if, všechny tři části for cyklu, atp.
  • před zavoláním funkce a před návratem z funkce
  • a některé další, týkající se např. inicializace proměnných či objektů

Všimněte si především toho, že v tomto seznamu nejsou další operátory, jako je např. přiřazení, sčítání, ++ apod.

V čem je tedy přesně problém?

Jak již bylo nastíněno v úvodu, problém je v tom, podle norem obou jazyků je pořadí vyhodnocování bočních efektů mezi jednotlivými sekvenčními body nedefinováno (5.4 v C++98). Přesněji, říká se tam následující:

Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored.

Pokud se tedy nyní podíváme na příklad uvedený pod nadpisem, a[i] = i++;, tak vidíme, že obsahuje pouze jeden sekvenční bod, a to ukončující středník. Mezi ním ovšem dochází jak k získání hodnoty proměnné i (a[i]), tak k bočnímu efektu nad i (i++). Výsledné chování je tedy nedefinované a po provedení příkazu nelze říci nic o výsledku tohoto příkazu.

Pokud si teď říkáte něco ve stylu "Počkat. Dejme tomu, že hodnota i je před tímto příkazem 1. Pak by se do a[1] měla uložit hodnota 1, a po provedení příkazu by mělo platit i == 2, ne? (Používáme přece postfixovou verzi operátoru ++, který hodnotu změní až po dokončení příkazu.)", tak vás zklamu. Postfixový operátor se od prefixového liší pouze tím, jakou hodnotu vrací (prefixový vrací novou hodnotu, postfixový vrací původní hodnotu). To, kdy dojde ke změně hodnoty, nezáleží na tom, kterou z verzí operátoru ++ použijete, ale na překladači.

Rada na závěr

Pokud si nejste jisti pořadím vyhodnocování podvýrazů v příkazu, tak jej raději rozepište do více příkazů. Jinak riskujete, že váš kód bude mít nedefinované chování. Kód pod nadpisem by měl být přepsán do následující podoby: a[i] = i; i++; (nejlépe však dát každý příkaz na samostatný řádek), která je, s ohledem na boční efekty, bezpečná.

Další informace

Další informace lze nalézt zde: 1 (doporučuji), 2, 3, 4, norma C++98 (sekce 1.9).

Komentáře

zrovna na takovy warning jsem nedavno narazil.. diky za vysvetleni..

Hehe. Na tohle jsem se uz taky nachytal. Zpusobuje to takove ty zajimave chyby typu "vedle o 1". Prochazel jsem nejakym polem a porad se zdalo, jako kdyz jsem o 1 index vedle. A ejhle, i++ nebo ++i nedela to co by se zdalo (vedlejsi efekty v jinou chvili nez jsem cekal). Obecne je asi lepsi psat jednodussi vyrazy a moc to neskladat. "Lepsi" texty o programovani doporucuji psat jednoduche vyrazy a ty moc slozene oznacuji za "bad code smell". Neco na tom bude :)

Přidat komentář