Sdílet prostřednictvím


Postupy: Rozhraní mezi výjimečným a jiným kódem než výjimečným

Tento článek popisuje, jak implementovat konzistentní zpracování výjimek v kódu C++ a jak přeložit výjimky na a z kódů chyb na hranicích výjimek.

Kód jazyka C++ se někdy musí shodovat s kódem, který nepoužívá výjimky (kód, který není výjimečný). Takové rozhraní se označuje jako hranice výjimky. Můžete například chtít volat funkci CreateFile Win32 v programu C++. CreateFile nevyvolá výjimky. Místo toho nastaví kódy chyb, které může funkce načíst GetLastError . Pokud váš program C++ není triviální, pravděpodobně budete chtít mít konzistentní zásady zpracování chyb na základě výjimek. A pravděpodobně nechcete opustit výjimky jen proto, že pro vás rozhraní s kódem, který není výjimečný. Nechcete také kombinovat zásady chyb založené na výjimce a na základě výjimek v kódu C++.

Volání nevýkonných funkcí z jazyka C++

Při volání nevýznamové funkce z jazyka C++je cílem zabalit tuto funkci do funkce jazyka C++, která detekuje případné chyby a následně vyvolá výjimku. Při návrhu takové obálkové funkce nejprve rozhodněte, jaký typ záruky výjimky má poskytnout: noexcept, strong nebo basic. Za druhé, navrhnout funkci tak, aby všechny prostředky, například popisovače souborů, byly správně uvolněny, pokud je vyvolán výjimka. Obvykle to znamená, že k vlastnictví prostředků používáte inteligentní ukazatele nebo podobné správce prostředků. Další informace o aspektech návrhu naleznete v tématu Postupy: Návrh pro bezpečnost výjimek.

Příklad

Následující příklad ukazuje funkce jazyka C++, které používají Win32 CreateFile a ReadFile funkce interně k otevření a čtení dvou souborů. Třída File je inicializační obálka (RAII) pro popisovače souboru. Jeho konstruktor zjistí podmínku "soubor nebyl nalezen" a vyvolá výjimku, která rozšíří chybu do zásobníku volání spustitelného souboru C++ (v tomto příkladu main() funkce). Pokud je výjimka vyvolán po úplném vytvoření objektu File , destruktor automaticky volá, CloseHandle aby uvolnil popisovač souboru. (Pokud chcete, můžete k tomuto účelu použít třídu KNIHOVNY ATL (Active Template Library) CHandle nebo unique_ptr společně s vlastní funkcí odstranění.) Funkce, které volají rozhraní API Win32 a CRT, detekují chyby a pak vyvolá výjimky jazyka C++ pomocí místně definované ThrowLastErrorIf funkce, která zase používá Win32Exception třídu odvozenou runtime_error z třídy. Všechny funkce v tomto příkladu poskytují záruku silné výjimky: Pokud je výjimka vyvolána v libovolném bodě v těchto funkcích, žádné prostředky se nevracejí a stav programu se nezmění.

// compile with: /EHsc
#include <Windows.h>
#include <stdlib.h>
#include <vector>
#include <iostream>
#include <string>
#include <limits>
#include <stdexcept>

using namespace std;

string FormatErrorMessage(DWORD error, const string& msg)
{
    static const int BUFFERLENGTH = 1024;
    vector<char> buf(BUFFERLENGTH);
    FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, 0, error, 0, buf.data(),
        BUFFERLENGTH - 1, 0);
    return string(buf.data()) + "   ("  + msg  + ")";
}

class Win32Exception : public runtime_error
{
private:
    DWORD m_error;
public:
    Win32Exception(DWORD error, const string& msg)
        : runtime_error(FormatErrorMessage(error, msg)), m_error(error) { }

    DWORD GetErrorCode() const { return m_error; }
};

void ThrowLastErrorIf(bool expression, const string& msg)
{
    if (expression) {
        throw Win32Exception(GetLastError(), msg);
    }
}

class File
{
private:
    HANDLE m_handle;

    // Declared but not defined, to avoid double closing.
    File& operator=(const File&);
    File(const File&);
public:
    explicit File(const string& filename)
    {
        m_handle = CreateFileA(filename.c_str(), GENERIC_READ, FILE_SHARE_READ,
            nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, nullptr);
        ThrowLastErrorIf(m_handle == INVALID_HANDLE_VALUE,
            "CreateFile call failed on file named " + filename);
    }

    ~File() { CloseHandle(m_handle); }

    HANDLE GetHandle() { return m_handle; }
};

size_t GetFileSizeSafe(const string& filename)
{
    File fobj(filename);
    LARGE_INTEGER filesize;

    BOOL result = GetFileSizeEx(fobj.GetHandle(), &filesize);
    ThrowLastErrorIf(result == FALSE, "GetFileSizeEx failed: " + filename);

    if (filesize.QuadPart < (numeric_limits<size_t>::max)()) {
        return filesize.QuadPart;
    } else {
        throw;
    }
}

vector<char> ReadFileVector(const string& filename)
{
    File fobj(filename);
    size_t filesize = GetFileSizeSafe(filename);
    DWORD bytesRead = 0;

    vector<char> readbuffer(filesize);

    BOOL result = ReadFile(fobj.GetHandle(), readbuffer.data(), readbuffer.size(),
        &bytesRead, nullptr);
    ThrowLastErrorIf(result == FALSE, "ReadFile failed: " + filename);

    cout << filename << " file size: " << filesize << ", bytesRead: "
        << bytesRead << endl;

    return readbuffer;
}

bool IsFileDiff(const string& filename1, const string& filename2)
{
    return ReadFileVector(filename1) != ReadFileVector(filename2);
}

#include <iomanip>

int main ( int argc, char* argv[] )
{
    string filename1("file1.txt");
    string filename2("file2.txt");

    try
    {
        if(argc > 2) {
            filename1 = argv[1];
            filename2 = argv[2];
        }

        cout << "Using file names " << filename1 << " and " << filename2 << endl;

        if (IsFileDiff(filename1, filename2)) {
            cout << "+++ Files are different." << endl;
        } else {
            cout<< "=== Files match." << endl;
        }
    }
    catch(const Win32Exception& e)
    {
        ios state(nullptr);
        state.copyfmt(cout);
        cout << e.what() << endl;
        cout << "Error code: 0x" << hex << uppercase << setw(8) << setfill('0')
            << e.GetErrorCode() << endl;
        cout.copyfmt(state); // restore previous formatting
    }
}

Volání výjimečného kódu z kódu, který není výjimečný

Funkce jazyka C++, které jsou deklarovány jako extern "C" volat programy jazyka C. Servery modelu COM jazyka C++ mohou být využity kódem napsaným v libovolném počtu různých jazyků. Při implementaci funkcí pracujících s veřejnými výjimkami v jazyce C++, které mají být volány jiným než výjimečným kódem, nesmí funkce jazyka C++ umožňovat, aby se do volajícího rozšířily žádné výjimky. Tito volající nemají způsob, jak zachytit nebo zpracovat výjimky jazyka C++. Program může ukončit, nevratit prostředky nebo způsobit nedefinované chování.

Doporučujeme, aby vaše extern "C" funkce jazyka C++ chytala každou výjimku, kterou ví, jak ji zpracovat, a v případě potřeby převeďte výjimku na kód chyby, kterému volající rozumí. Pokud nejsou známy všechny potenciální výjimky, funkce C++ by měla mít blok jako poslední obslužnou catch(...) rutinu. V takovém případě je nejlepší oznámit volajícímu závažnou chybu, protože váš program může být v neznámém a neopravitelném stavu.

Následující příklad ukazuje funkci, která předpokládá, že jakákoli výjimka, která může být vyvolán, je buď Win32Exception typ výjimky odvozený z std::exception. Funkce zachytí všechny výjimky z těchto typů a rozšíří informace o chybě jako kód chyby Win32 volajícímu.

BOOL DiffFiles2(const string& file1, const string& file2)
{
    try
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return FALSE;
        }
        return TRUE;
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }

    catch(std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return FALSE;
}

Při převodu z výjimek na kódy chyb existuje potenciální problém: Kódy chyb často neobsahují bohatost informací, které může výjimka uložit. Chcete-li tento problém vyřešit, můžete zadat catch blok pro každý konkrétní typ výjimky, který může být vyvolán, a provést protokolování pro zaznamenání podrobností o výjimce před převodem na kód chyby. Tento přístup může vytvořit opakující se kód, pokud všechny funkce používají stejnou sadu catch bloků. Dobrým způsobem, jak se vyhnout opakování kódu, je refaktoring těchto bloků do jedné funkce privátního nástroje, která implementuje try a blokuje a catch přijímá objekt funkce, který je vyvolán v try bloku. V každé veřejné funkci předejte kód funkci nástroje jako výraz lambda.

template<typename Func>
bool Win32ExceptionBoundary(Func&& f)
{
    try
    {
        return f();
    }
    catch(Win32Exception& e)
    {
        SetLastError(e.GetErrorCode());
    }
    catch(const std::exception& e)
    {
        SetLastError(MY_APPLICATION_GENERAL_ERROR);
    }
    return false;
}

Následující příklad ukazuje, jak napsat výraz lambda, který definuje functor. Výraz lambda je často čitelnější než kód, který volá pojmenovaný objekt funkce.

bool DiffFiles3(const string& file1, const string& file2)
{
    return Win32ExceptionBoundary([&]() -> bool
    {
        File f1(file1);
        File f2(file2);
        if (IsTextFileDiff(f1, f2))
        {
            SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
            return false;
        }
        return true;
    });
}

Další informace o výrazech lambda najdete v tématu Výrazy lambda.

Volání výjimečného kódu prostřednictvím kódu, který není výjimečný z výjimečného kódu

Je možné, ale nedoporučuje se hodit výjimky v kódu bez výjimky. Například program C++ může volat knihovnu, která používá funkce zpětného volání, které zadáte. Za některých okolností můžete vyvolat výjimky z funkcí zpětného volání v kódu, který nedokáže zpracovat původní volající. Okolnosti, kdy mohou výjimky úspěšně fungovat, jsou však přísné. Kód knihovny je nutné zkompilovat způsobem, který zachovává sémantiku odvíjení zásobníku. Kód bez výjimky nemůže udělat nic, co by mohlo výjimku C++ vystihot. Kód knihovny mezi volajícím a zpětným voláním také nemůže přidělit místní prostředky. Například kód, který není s vědomím výjimky, nemůže mít místní hodnoty, které odkazují na přidělenou paměť haldy. Tyto prostředky se nevracejí, když je zásobník unwound.

Tyto požadavky musí být splněny, aby se vyvolaly výjimky v kódu bez výjimky:

  • Můžete vytvořit celou cestu kódu napříč kódem, který nezná výjimky, pomocí metody /EHs,
  • Neexistují žádné místně přidělené prostředky, které by mohly dojít k úniku, když je zásobník unwound,
  • Kód neobsahuje žádné __except strukturované obslužné rutiny výjimek, které zachytí všechny výjimky.

Vzhledem k tomu, že vyvolání výjimek v případě, že kód není výjimečný, je náchylný k chybám a může způsobit obtížné problémy s laděním, nedoporučujeme ho.

Viz také

Moderní osvědčené postupy jazyka C++ pro výjimky a zpracování chyb
Postupy: Návrh pro bezpečnost výjimek