Hodně programátorů, kteří chtějí psát své programy čistě, se občas zabývají otázkou, jak nejlépe ošetřovat chyby, aby kód byl stále přehledný. Drtivá většina API funkcí Windows buďto vrací chybu, nebo ji umožňuje zjistit pomocí volání funkce GetLastError(). Tento článek shrnuje moje zkušenosti s ošetřováním chyb při volání API funkcí. Rád bych zdůraznil, že vhodnost nebo nevhodnost jednotlivých možností uvedených v tomto článku mohou být subjektivní, a nemusí s nimi každý souhlasit.
Ošetřování chyb si vyzkoušíme na jednoduchém příkladu kopírování souboru. Za předpokladu, že nechceme použít funkci CopyFile(), která provede vše v jednom kroku, musíme provést následující
V bodech 1-3 by mohla nastat chyba, která způsobí, že kopírování nebude dokončeno v pořádku. V případě výskytu chyb by měl program korektně reagovat, uvolnit alokované prostředky a oznámit chybu uživateli.
Podívejme se, jak by takový postup vypadal, kdybychom nemuseli ošetřovat chyby:
HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, 0, NULL);
VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize);
while(dwTransferred != 0)
{
ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL);
if(dwTransferred != 0)
WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL);
}
HeapFree(GetProcessHeap(), 0, lpBuffer);
CloseHandle(hFileTrg);
CloseHandle(hFileSrc);
Program je relativně krátký, a (snad) i čitelný. Bohužel ne vždy funkční. Nikdo nám nezaručí, že zdrojový soubor půjde otevřít. Nevíme, zda existuje, a i když existuje, může být chráněn proti otevření. Stejně tak nevíme, zda se nám podaří vytvořit cílový soubor. A alokace paměti může selhat, pokud se např. pokusíme alokovat příliš velký kus, nebo pokud je paměti celkově málo (i když v takovém případě se nejspíš zbortí celý systém).
První metoda ošetření chyb spočívá v uvolnění alokovaných prostředků bezprostředně po zjištění chyby a v následném návratu z funkce:
// Otevřeme zdrojový soubor
HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if(hFileSrc == INVALID_HANDLE_VALUE)
return GetLastError();
// Otevřeme cílový soubor
HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, 0, NULL);
if(hFileTrg == INVALID_HANDLE_VALUE)
{
nError = GetLastError();
CloseHandle(hFileSrc);
return nError;
}
// Alokujeme paměť
VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize);
if(lpBuffer == NULL)
{
nError = GetLastError();
CloseHandle(hFileTrg);
CloseHandle(hFileSrc);
return nError;
}
// Překopírujeme obsah souboru
while(dwTransferred != 0)
{
ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL);
if(dwTransferred != 0)
WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL);
}
// Uvolníme alokované prostředky a návrat
HeapFree(GetProcessHeap(), 0, lpBuffer);
CloseHandle(hFileTrg);
CloseHandle(hFileSrc);
Tato metoda je provedena čistě, žádná chyba nezůstane neošetřena. Nepříjemné je ale to, že v kódu na několik místech voláme uzavírání souborů, které nesmíme za žádnou potenciálně chybnou operací zapomenout. Pokud ještě navíc při udržování kódu vložíme další mezikrok, budeme muset přidat uvolňování prostředků i za tento mezikrok (pokud skončí s chybou). Navíc do každého dalšího kroku musíme vložit ještě uvolňování prostředků vzniklých tímto mezikrokem.
Můžeme také napsat kód tak, aby byl další krok proveden jedině v případě, kdy se nepodaří provést krok předchozí. Kód může vypadat třeba takto:
// Otevřeme zdrojový soubor
HANDLE hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if(hFileSrc != INVALID_HANDLE_VALUE)
{
// Otevřeme cílový soubor
HANDLE hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, 0, NULL);
if(hFileTrg != INVALID_HANDLE_VALUE)
{
// Alokujeme paměť
VOID * lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize);
if(lpBuffer != NULL)
{
DWORD dwTransferred = 1;
// Překopírujeme obsah souboru
while(dwTransferred != 0)
{
ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL);
if(dwTransferred != 0)
WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL);
}
// Uvolníme paměť
HeapFree(GetProcessHeap(), 0, lpBuffer);
}
else
nError = GetLastError();
// Uzavřeme cílový soubor
CloseHandle(hFileTrg);
}
else
nError = GetLastError();
// Uzavřeme zdrojový soubor
CloseHandle(hFileSrc);
}
else
nError = GetLastError();
Dobrá metoda, žádné volání tam není navíc. Kód vypadá přehledně. Pokud by ale měl počet kroků být větší (10 nebo více), tak se odsazení nejvíce vnořených částí pohybuje v polovině používaných editorů (pokud odsazujete - jako já - po čtyřech mezerách). Na samotný zdrojový text už zbývá poměrně málo místa (pokud ovšem nepoužíváte rozlišení 1600 na 1200, kdy jsou písmenka tak maličká, že nejsou skoro poznat :-)).
K ošetření chyb je možné využít i strukturovaných vyjímek - např. pomocí bloku try-finally. Pokud blok try jakýmkoliv způsobem opustíme, vždy se nejdříve spustí ještě blok finally. V tomto bloku provedeme úklid. Abychom ale poznali, kterou proměnnou je třeba "uklidit", musíme na začátku funkce všechny použité proměnné initializovat na neplatnou hodnotu - ukazatele na NULL, handly na NULL nebo na hodnotu INVALID_HANDLE_VALUE:
HANDLE hFileSrc = INVALID_HANDLE_VALUE;
HANDLE hFileTrg = INVALID_HANDLE_VALUE;
LPVOID lpBuffer = NULL;
__try
{
// Otevřeme zdrojový soubor
hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if(hFileSrc == INVALID_HANDLE_VALUE)
return GetLastError();
// Otevřeme cílový soubor
hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, 0, NULL);
if(hFileTrg == INVALID_HANDLE_VALUE)
return GetLastError();
// Alokujeme paměť
lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize);
if(lpBuffer == NULL)
return GetLastError();
// Překopírujeme obsah souboru
while(dwTransferred != 0)
{
ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL);
if(dwTransferred != 0)
WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL);
}
}
__finally
{
// Uvolníme prostředky
if(lpBuffer != NULL)
HeapFree(GetProcessHeap(), 0, lpBuffer);
if(hFileTrg != INVALID_HANDLE_VALUE)
CloseHandle(hFileTrg);
if(hFileSrc != INVALID_HANDLE_VALUE)
CloseHandle(hFileSrc);
}
return ERROR_SUCCESS;
Poslední zde uvedená metoda je podobná té předchozí, jenom nepoužívá block try-except. Každý krok se provede pouze tehdy, když všechny předchozí skončily úspěšně.
HANDLE hFileSrc = INVALID_HANDLE_VALUE;
HANDLE hFileTrg = INVALID_HANDLE_VALUE;
LPVOID lpBuffer = NULL;
int nError = ERROR_SUCCESS;
// Otevřeme zdrojový soubor
if(nError == ERROR_SUCCESS)
{
hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if(hFileSrc == INVALID_HANDLE_VALUE)
nError = GetLastError();
}
// Otevřeme cílový soubor
if(nError == ERROR_SUCCESS)
{
hFileTrg = CreateFile(szFileTrg, GENERIC_WRITE, 0,
NULL, CREATE_ALWAYS, 0, NULL);
if(hFileTrg == INVALID_HANDLE_VALUE)
nError = GetLastError();
}
// Alokujeme paměť
if(nError == ERROR_SUCCESS)
{
lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwBuffSize);
if(lpBuffer == NULL)
nError = GetLastError();
}
// Překopírujeme obsah souboru
if(nError == ERROR_SUCCESS)
{
while(dwTransferred != 0)
{
ReadFile(hFileSrc, lpBuffer, dwBuffSize, &dwTransferred, NULL);
if(dwTransferred != 0)
WriteFile(hFileTrg, lpBuffer, dwTransferred, &dwTransferred, NULL);
}
}
// Uvolníme prostředky
if(lpBuffer != NULL)
HeapFree(GetProcessHeap(), 0, lpBuffer);
if(hFileTrg != NULL)
CloseHandle(hFileTrg);
if(hFileSrc != NULL)
CloseHandle(hFileSrc);
return nError;
Uvedené metody ošetřování chyb berte pouze jako tip, jak ošetřovat chyby. Z praxe vím, že každý programátor to dělá po svém - většinou začíná s první metodou, ale brzy zjistí, že moc nevyhovuje, a tak na chyby vracené API funkcemi nějak reaguje. Pokud budete programovat profesionální aplikaci většího rozsahu, která by měla být "neshoditelná", tedy velice stabilní, většinou se toho lépe dosahuje s aplikací, která důsledně testuje vracené chyby. Nepředpokládejte, že nějaká funkce vždy skončí s úspěchem - vždy je možné, že soubor, klíč registru nebo adresář nelze otevřít/vytvořit, nebo že nemáme k dispozici dostatek paměti.
Copyright Ladislav Zezula 11.09.2003