Problematikou přetečení jsme se v našem seriálu již zabývali, když jsme si vysvětlovali přepínač Range checking. Tento přepínač však nepředstavuje jedinou možnost, jak se zabývat přetečením proměnných a výsledků operací. Dnešní článek je opět zaměřen na popis přetečení, avšak namísto Range checking bude pro nás aktuální Overflow checking. Víte, jaký je mezi nimi rozdíl?
Dnešní článek bude obsahovat pokračování naší aktuálně rozebírané tématiky. V současnosti se zabýváme popisem jednotlivých možností, jak nastavit překladač Delphi a jak tímto způsobem ovlivnit výsledný přeložený program (tedy soubor *.EXE) k obrazu svému. V každém díle vždy popíšeme jeden nebo více přepínačů z dialogu Project – Options, záložka Compiler. V souvislosti s příslušným přepínačem se vždy podrobněji dostaneme k odpovídající problematice, kterou tak rozebereme. Tímto způsobem jsme se tedy zabývali optimalizacemi zdrojového kódu, přetékáním rozsahů proměnných nebo problematikou testování chyb při práci se soubory.
Na samotný úvod opět shrnu, které přepínače ze stránky Compiler dialogu Project – Options jsme již prošli a které nás teprve čekají. Následující přepínače již máme za sebou:
- skupina Code generation: přepínač Optimization (slouží k zapnutí/vypnutí optimalizace, která se projeví vypuštěním nepotřebných proměnných z výsledného programu),
- skupina Code generation: přepínač Aligned recorded fields (slouží k zapnutí/vypnutí zarovnávání struktur v paměti do 32-bitových bloků),
- skupina Code generation: přepínač Stack frames (slouží k ovlivnění způsobu ukládání parametrů procedur a funkcí do zásobníku),
- skupina Code generation: přepínač Pentium-safe FDIV (slouží k zapnutí/vypnutí generování speciálního kódu odolného vůči chybě při dělení v plovoucí řádové čárce u starších procesorů Pentium),
- skupina Runtime errors: přepínač Range checking (slouží k zapnutí/vypnutí hlídání rozsahů proměnných),
- skupina Runtime errors: přepínač I/O chceking (slouží k zapnutí/vypnutí automatického testování chyb při vstupně/výstupních operacích, tj. především při práci se soubory).
Podíváte-li se do zmíněného dialogu pro nastavování možností překladače, jistě sami uzříte, že dalším přepínačem v pořadí je Overflow checking, tím tedy dnes začneme.
Skupina Runtime errors – pokračování
Přepínač Overflow checking
Přepínač Overflow checking je poměrně dosti podobný přepínači Range checking, kterým jsme se v našem seriálu zabývali před dvěma týdny. Pokud je tento přepínač (který mimochodem odpovídá direktivě {$Q}) zapnutý, generuje překladač takový programový kód, který hlídá, jestli náhodou výsledky některých aritmetických funkcí nepřekročí povolený rozsah příslušné proměnné.
Než se na Overflow checking (a na jeho rozdíl oproti Range checking) podíváme podrobněji, opět poznamenám, že zapnutí této volby způsobí taktéž částečné zpomalení provádění programu, proto opět obecně platí, že bychom se měli rozhodnout mezi testováním přetečení (volba zapnuta) a mezi požadavkem na vyšší rychlost aplikace (volba vypnuta). Vhodné doporučení by mohlo znít třeba takto: rozhodně si ponechte testování zapnuté ve fázi ladění; v okamžiku šíření aplikace je možné (po důkladném, důsledném a dlouhodobém testování) tuto volbu vypnout. V případě, že dochází k jakýmkoliv „podivným“ chybám, je možné tuto volbu zase zapnout a pokusit se zjistit, zda chyby nejsou způsobeny přetékáním hodnot.
Na druhou stranu, pokud neprogramujete zrovna nějakou časově kritickou aplikaci, je možné, že o pár procent nižší výkon není žádnou tragédií. V takovém případě je bez problémů možné ponechat testování zapnuté i ve verzi aplikace určené pro šíření. A nebo je možné v případě pochybností udělat několik testů, změřit rychlost aplikace a následně se rozhodnout, jak se k Overflow checking (a k Range checking) postavit.
Nyní se dostáváme k rozdílu mezi přepínači Overflow checking (direktiva {$R}) a přepínači Range checking (direktiva {$R}). Protože tento rozdíl je možná poněkud nejasný, zastavíme se na tomto místě opět o trochu déle a pokusíme se vysvětlit vše podstatné.
Učiníme několik pokusů. Vytvořte v Delphi nový program, na formulář umístěte jedno tlačítko a dva seznamy ListBox. Ošetřete událost OnClick tlačítka Button1 takto:
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
b := 0; c := 0;
for a := 0 to 300 do begin
try
b := b + 1;
ListBox1.Items.Insert(0, `b = ` + IntToStr(b));
except
on E:Exception do begin
ListBox2.Items.Insert(0, `+ : ` + E.Message + `, b = ` + IntToStr(b));
end;
end;
try
Inc(c);
ListBox1.Items.Insert(0, `c = ` + IntToStr(c));
except
on E:Exception do begin
ListBox2.Items.Insert(0, `Inc : ` + E.Message + ` , c = ` + IntToStr(c));
end;
end;
end;
end;
Co tato procedura dělá? Především: používáme dvě pomocné proměnné typu Byte (proměnné b, c). Datový typ Byte je uložen na osmi bitech, proto umožňuje zobrazit pouze hodnoty 0 až 255. Pro naše účely bude ideální.
Princip procedury je jednoduchý: pokusíme se uvnitř cyklu for inkrementovat obě pomocné proměnné typu Byte, a to až do hodnoty 300. Protože je očividné, že tato hodnota je pro datový typ Byte příliš veliká, je zřejmé, že se něco stane :-)
Co se stane, to si nejprve vysvětlíme, a pak ukážeme prakticky. Abychom dobře viděli rozdíl mezi volbami Overflow checking a Range checking a abychom si ozřejmili, které výjimky jsou generovány v souvislosti s nimi, umístil jsem do zdrojového kódu dvě chráněné sekce Try. V první sekci je umístěno obyčejné přičtení jedničky (b := b + 1), zatímco ve druhé chráněné sekci je použita funkce Inc (která způsobí také zvýšení hodnoty o jedničku).
V prvním seznamu (ListBox1) se vypisují hodnoty proměnných b a c, tak, jak jsou průběžně inkrementovány (včetně případného přetečení a pokračování znovu „od nuly“). Ve druhém seznamu (ListBox2) jsou pak vypisována chybová hlášení, která vzniknou uvnitř chráněných sekcí. Sekce except, které následujíc za chráněnými sekcemi, jsou konstruovány tak, aby vypsaly nejen typ vzniklé výjimky, ale také aktuální hodnotu proměnné, u níž k výjimce došlo, a také příkaz, který výjimku vygeneroval (tj. buď operaci + nebo funkci Inc).
Oba seznamy používají pro přidávání nových položek nikoliv metodu Items.Add, ale Items.Insert, díky čemuž jsou nové položky přidávány na začátek seznamu (na pozici 0).
Nyní se pokusíme aplikaci několikrát přeložit a spustit při různých nastaveních překladače. Nejprve tedy vypneme oba druhy testování (Range checking i Overflow checking). Aby bylo vše úplně jasné, budeme opět manipulaci s přepínači provádět pomocí direktiv – a tedy ve zdrojovém kódu:
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
{$R-}
{$Q-}
b := 0; c := 0;
for a := 0 to 300 do begin
... další kód je beze změn
Připomeňme, že aplikaci spouštíme bez jakýchkoliv testů, bez hlídání přetečení u proměnných i u aritmetických operací. Pokud nyní aplikaci spustíme, bude výsledek vypadat takto (viz následující obrázek):
Co tento výsledek znamená? Především: v průběhu operace nedošlo k žádné chybě, nebyla generována ani jedna výjimka, protože druhý (spodní) seznam je prázdný. Pokud si „odrolujete“ prvním seznamem dolů, uvidíte, že cyklus for opravdu proběh třistakrát; po 256 průchodech přetekly hodnoty proměnných b a c zpět na nulu, takže výsledné hodnoty po dokončení cyklu jsou v obou případech 45.
Na tomto místě je opět vhodné upozornit, že pokud se nám „podaří“ si do své aplikace zavléct takovouto chybu, bude její odhalení nesmírně obtížné. Důvodem je především skutečnost, že chybné výsledky se nemusí projevit vždy (ale třeba jen při „šikovné“ kombinaci vstupních dat) a kromě toho se typicky objeví na úplně jiném místě, než bychom čekali. Samozřejmostí – k dovršení všech Murphyho zákonů – pak bude, že chybné výsledky se objeví u úplně jiných proměnných než u těch, které ve skutečnosti přetekly (neboť s největší pravděpodobností nám přetečou nějaké pomocné proměnné, jejichž hodnoty nikde nevypisujeme, jen s nimi na několika místech pracujeme).
Předchozí odstavec by se dal také shrnout do jediné věty: další důvod pro ponechávání Overflow checking a Range checking „v provozu“.
Nyní však pokračujme v procházce naší aplikací. Pokusíme se zapnout jednu z voleb a začneme třeba volbou Range checking. Upravíme tedy programový kód takto:
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
{$R+}
{$Q-}
b := 0; c := 0;
for a := 0 to 300 do begin
... další programový kód je již beze změn
Zapnuli jsme přepínač {$R} a tím jsme zapnuli Range checking. Spustíme aplikaci a uvidíme, co se stane (viz následující obrázek):
Výsledky jsou velmi zajímavé. Všimněte si, že hodnota proměnné c se nijak nezměnila od předchozího případu. Proměnná c je plněna aritmetickou funkcí Inc a přepínač Range checking nemá na kontrolu jejího přetečení žádný vliv.
Naproti tomu proměnná b se „zastavila“ na hodnotě 255; přepínač Range checking nedovolí její přetečení pomocí operátoru +, naopak při každém pokusu o něco takového vygeneruje výjimku Range check error, jak je patrné ze seznamu ListBox2.
Pojďme dále. Vypneme opět Range checking a zapneme pro změnu Overflow checking, viz následující zdrojový kód:
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
{$R-}
{$Q+}
b := 0; c := 0;
for a := 0 to 300 do begin
... další programový kód je již beze změn
Nyní jsme zapnuli Overflow checking a vypnuli Range checking. Aplikaci opět spustíme a zkontrolujeme výsledky, viz následující obrázek:
Nyní jsou výsledky proměnných podobné jako v prvním případě, kdy jsme nic netestovali, ale v tomto případě jsme se dozvěděli, že v okamžiku, kdy se hodnota proměnné c (která je inkrementována aritmetickou funkcí Inc) přehoupla zpět na nulu, došlo k vygenerování výjimky Integer overflow.
Dozvěděli jsme se tedy, že došlo k jakési chybě, což může být často poměrně důležité. Všimněte si, že přetékání proměnné b (které bylo způsobeno používáním operátoru +) nechalo přepínač Overflow checking zcela chladným: proměnné b přetekla a vyhoupla se na 45, aniž by byla generována jakákoliv výjimka.
Poslední varianta, kterou nám zbývá prozkoumat, už asi nikoho příliš nepřekvapí. A nepřekvapí nás ani její výsledky, které se daly také očekávat. Zapneme tedy oba druhy testování, Range checking i Overflow checking, viz následující zdrojový kód:
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
{$R+}
{$Q+}
b := 0; c := 0;
for a := 0 to 300 do begin
... další programový kód je již beze změn
Pokud tuto verzi přeložíme a spustíme, budou výsledky podle očekávání, viz následující obrázek:
Seznam ListBox2 je na tomto obrázku odrolován na samotný začátek, aby bylo patrné, že obsahuje i výjimku Integer overflow vzniklou při zavolání funkce Inc.
Asi nikoho nepřekvapí, že v tomto případě jsou odchyceny oba druhy výjimek, tedy výjimky vzniklé při přetečení proměnných (Range checking) i výjimky vzniklé při přetečení aritmetických operací (Overflow checking).
Jak tedy celé dnešní téma uzavřít? Pokud si chceme být jisti, že aplikace si bude „hlídat“ přetečení všech typů, je lepší zapnout testování Range checking i Overflow checking. Overflow checking slouží k testování přetečení způsobeného aritmetickými funkcemi.
Zdrojový kód
Na závěr opět uvedeme kompletní zdrojový kód aplikace v její poslední verzi, tj. při zapnutém testování Overflow checking i Range checking. Součástí aplikace je kromě výše popisované obsluhy události Button1Click také ošetření události Form1.OnCreate, kde ale pouze nastavíme titulek formuláře a titulek tlačítka.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
ListBox1: TListBox;
ListBox2: TListBox;
procedure Button1Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var a: integer;
b, c: byte;
begin
{$R+}
{$Q+}
b := 0; c := 0;
for a := 0 to 300 do begin
try
b := b + 1;
ListBox1.Items.Insert(0, `b = ` + IntToStr(b));
except
on E:Exception do begin
ListBox2.Items.Insert(0, `+ : ` + E.Message + `, b = ` + IntToStr(b));
end;
end;
try
Inc(c);
ListBox1.Items.Insert(0, `c = ` + IntToStr(c));
except
on E:Exception do begin
ListBox2.Items.Insert(0, `Inc : ` + E.Message + ` , c = ` + IntToStr(c));
end;
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Caption := `Testovani Range checking a Overflow checking`;
Button1.Caption := `Start`;
end;
end.
Na závěr
Tolik k testování přetečení proměnných a výsledků operací. Dnešním článkem, který ukázal rozdíly mezi Range checking a overflow checking, jsme také uzavřeli další sekci v dialogu Project – Options, záložka Compiler. Touto sekcí je Runtime errors, kde jsme se již zabývali všemi třemi přepínači.