Condividi tramite


Procedura: Interfaccia tra codice eccezionale e non eccezionale

Questo articolo descrive come implementare una gestione coerente delle eccezioni nel codice C++ e come convertire le eccezioni da e verso codici di errore ai limiti delle eccezioni.

A volte il codice C++ deve interfacciarsi con il codice che non usa eccezioni (codice non eccezionale). Tale interfaccia è nota come limite di eccezione. Ad esempio, è necessario chiamare la funzione Win32 CreateFile nel programma C++. CreateFile non genera eccezioni. Imposta invece i codici di errore che possono essere recuperati dalla GetLastError funzione. Se il programma C++ non è semplice, è probabile che si preferisca avere un criterio di gestione degli errori coerente basato su eccezioni. E probabilmente non si vogliono abbandonare le eccezioni solo perché si interfaccia con codice non eccezionale. Non è inoltre necessario combinare criteri di errore basati su eccezioni e non basati su eccezioni nel codice C++.

Chiamare funzioni non eccezionali da C++

Quando si chiama una funzione senza eccezioni da C++, lo scopo è quello di eseguire il wrapping della funzione in una funzione C++ che rileva eventuali errori e quindi genera eventualmente un'eccezione. Quando si progetta una funzione wrapper di questo tipo, stabilire prima di tutto quale tipo di eccezione garantire: noexcept, strong o basic. In secondo luogo, occorre progettare la funzione in modo che tutte le risorse, ad esempio, gli handle dei file, vengano correttamente rilasciate se viene generata un'eccezione. In genere, significa che si usano puntatori intelligenti o gestori di risorse simili per possedere le risorse. Per altre informazioni sulle considerazioni sulla progettazione, vedere Procedura: Progettare per la sicurezza delle eccezioni.

Esempio

Nell'esempio seguente vengono mostrate le funzioni C++ che utilizzano le funzioni Win32 CreateFile e ReadFile internamente per aprire e leggere due file. La classe File rappresenta un wrapper RAII per gli handle di file. Il costruttore rileva una condizione "file non trovato" e genera un'eccezione per propagare l'errore nello stack di chiamate dell'eseguibile C++ (in questo esempio la main() funzione). Se un'eccezione viene generata dopo che un oggetto File è completamente costruito, il distruttore chiama automaticamente CloseHandle per rilasciare l'handle di file. Se si preferisce, è possibile usare la classe ATL (Active Template Library) CHandle per lo stesso scopo o un insieme unique_ptr a una funzione di eliminazione personalizzata. Le funzioni che chiamano le API Win32 e CRT rilevano gli errori e quindi generano eccezioni C++ usando la funzione definita ThrowLastErrorIf localmente, che a sua volta usa la Win32Exception classe derivata dalla runtime_error classe . Tutte le funzioni di questo esempio forniscono una garanzia di eccezione assoluta: se viene generata un'eccezione in qualsiasi punto in queste funzioni, non vengono perse risorse e non viene modificato alcuno stato del programma.

// 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
    }
}

Chiamare codice eccezionale da codice non eccezionale

Funzioni C++ dichiarate come extern "C" possono essere chiamate dai programmi C. I server COM C++ possono essere utilizzati dal codice scritto in un numero qualsiasi di linguaggi diversi. Quando si implementano in C++ funzioni pubbliche che supportano le eccezioni da chiamare in un codice senza eccezioni, la funzione C++ non deve consentire alle eccezioni di propagarsi fino al chiamante. Tali chiamanti non possono intercettare o gestire eccezioni C++. Il programma può terminare, perdere risorse o causare un comportamento non definito.

È consigliabile che la extern "C" funzione C++ intercetta in modo specifico ogni eccezione che sa come gestire e, se appropriato, convertire l'eccezione in un codice di errore compreso dal chiamante. Se non tutte le potenziali eccezioni sono note, la funzione C++ deve contenere un blocco catch(...) come ultimo gestore. In questo caso, è consigliabile segnalare un errore irreversibile al chiamante, perché il programma potrebbe trovarsi in uno stato sconosciuto e irreversibile.

Nell'esempio seguente viene illustrata una funzione che presuppone che qualsiasi eccezione generata sia un Win32Exception tipo di eccezione o un tipo di eccezione derivato da std::exception. La funzione intercetta tutte le eccezioni di questi tipi e propaga fino al chiamante l'informazione sull'errore, come un codice di errore Win32.

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;
}

Quando si esegue la conversione da eccezioni a codici di errore, si verifica un potenziale problema: i codici di errore spesso non contengono la ricchezza di informazioni che un'eccezione può archiviare. Per risolvere questo problema, è possibile fornire un catch blocco per ogni tipo di eccezione specifico che potrebbe essere generato ed eseguire la registrazione per registrare i dettagli dell'eccezione prima che venga convertita in un codice di errore. Questo approccio può creare codice ripetitivo se più funzioni usano tutti lo stesso set di catch blocchi. Un buon modo per evitare la ripetizione del codice consiste nel effettuare il refactoring di tali blocchi in una funzione di utilità privata che implementa i try blocchi e catch e accetta un oggetto funzione richiamato nel try blocco. In ciascuna funzione pubblica passare il codice alla funzione di utilità come un'espressione 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;
}

Di seguito viene illustrato come scrivere un'espressione lambda che definisce il funtore. Un'espressione lambda è spesso più semplice da leggere inline rispetto al codice che chiama un oggetto funzione denominato.

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;
    });
}

Per altre informazioni sulle espressioni lambda, vedere Espressioni lambda.

Chiamare codice eccezionale tramite codice non eccezionale da codice eccezionale

È possibile, ma non consigliato, generare eccezioni nel codice non a conoscenza delle eccezioni. Ad esempio, il programma C++ può chiamare una libreria che usa funzioni di callback fornite. In alcune circostanze, è possibile generare eccezioni dalle funzioni di callback nel codice non eccezionale che il chiamante originale può gestire. Tuttavia, le circostanze in cui le eccezioni possono funzionare correttamente sono rigide. È necessario compilare il codice della libreria in modo da mantenere la semantica di rimozione dello stack. Il codice di eccezione non è in grado di eseguire alcuna operazione che potrebbe intercettare l'eccezione C++. Inoltre, il codice della libreria tra il chiamante e il callback non può allocare risorse locali. Ad esempio, il codice che non è compatibile con le eccezioni non può avere variabili locali che puntano alla memoria heap allocata. Queste risorse vengono perse quando lo stack viene scollegato.

Questi requisiti devono essere soddisfatti per generare eccezioni nel codice non compatibile con le eccezioni:

  • È possibile compilare l'intero percorso del codice nel codice che non riconosce eccezioni usando /EHs,
  • Non sono presenti risorse allocate localmente che possono causare perdite quando lo stack viene sbloccato,
  • Il codice non dispone __except di gestori di eccezioni strutturati che intercettano tutte le eccezioni.

Poiché la generazione di eccezioni in codice non eccezionale è soggetta a errori e può causare problemi di debug difficili, non è consigliabile.

Vedi anche

Procedure consigliate C++ moderne per le eccezioni e la gestione degli errori
Procedura: Progettare per la sicurezza delle eccezioni