Ošetřování chyb při programování ve Windows

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.

Testovací příklad

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í

  1. Otevřít zdrojový soubor
  2. Otevřít nebo vytvořit cílový soubor
  3. Alokovat paměť na přenos (Mohli bychom použít lokální buffer, ale z "pedagogických" důvodů budeme alokovat pamět)
  4. Opakovaně číst ze vstupního souboru a ulkládat data do cílového souboru.
  5. Uvolnit paměť
  6. Uzavřít cílový soubor
  7. Uzavřít zdrojový soubor
  8. Pokud nastala chyba, oznámit ji uživateli

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.

Bez ošetření chyb

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

Ošetření chyb - první pokus

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.

Ošetření chyb - třetí pokus

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

Ošetření chyb - čtvrtý pokus

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;

Ošetření chyb - poslední pokus

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;

Závěrem

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.