Compartilhar via


Como realizar a interface entre códigos excepcionais e não excepcionais

Este artigo descreve como implementar o tratamento consistente de exceções no código C++ e como converter exceções de e para códigos de erro em limites de exceção.

Às vezes, o código C++ precisa fazer interface com código que não usa exceções (código não excepcional). Essa interface é conhecida como um limite de exceção. Por exemplo, talvez você queira chamar a função Win32 CreateFile em seu programa C++. CreateFile não gera exceções. Em vez disso, ela define códigos de erro que podem ser recuperados pela função GetLastError. Se o programa C++ não for trivial, você provavelmente preferirá ter uma política consistente de tratamento de erros baseada em exceções. E, provavelmente, você não deseja abandonar as exceções apenas porque você faz interface com código não excepcional. Você também não deseja misturar políticas de erro baseadas em exceção e não baseadas em exceção no seu código C++.

Chamar funções não excepcionais do C++

Quando você chama uma função não excepcional do C++, a ideia é encapsular essa função em uma função C++ que detecta erros e, possivelmente, gera uma exceção. Ao criar essa função wrapper, primeiro decida qual tipo de garantia de exceção fornecer: noexcept, strong ou basic. Em segundo lugar, projete a função para que todos os recursos, por exemplo, identificadores de arquivo, sejam liberados corretamente se uma exceção for gerada. Normalmente, isso significa que você usa ponteiros inteligentes ou gerenciadores de recursos semelhantes para ser proprietário dos recursos. Para obter mais informações sobre considerações de design, confira Como projetar tendo em vista a segurança da exceção.

Exemplo

O exemplo a seguir mostra funções C++ que usam as funções Win32 CreateFile e ReadFile internamente para abrir e ler dois arquivos. A classe File é um wrapper RAII (aquisição de recurso é inicialização) para os identificadores de arquivo. Seu construtor detecta uma condição de "arquivo não encontrado" e gera uma exceção para propagar o erro até a pilha de chamadas do executável C++ (neste exemplo, a função main()). Se uma exceção for gerada depois que um objeto File for totalmente construído, o destruidor chamará CloseHandle automaticamente para liberar o identificador de arquivo. (Se preferir, você poderá usar a classe ATL (Active Template Library) CHandle para essa mesma finalidade ou um unique_ptr com uma função de exclusão personalizada.) As funções que chamam as APIs Win32 e CRT detectam erros e geram exceções C++ usando a função ThrowLastErrorIf definida localmente, que, por sua vez, usa a classe Win32Exception, derivada da classe runtime_error. Todas as funções neste exemplo fornecem uma forte garantia de exceção: se uma exceção for gerada em qualquer ponto dessas funções, nenhum recurso será vazado e nenhum estado de programa será modificado.

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

Chamar código excepcional de código não excepcional

Funções C++ declaradas como extern "C" podem ser chamadas por programas C. Servidores C++ COM podem ser consumidos por código escrito em qualquer número de linguagens de programação diferentes. Quando você implementa funções públicas com reconhecimento de exceção no C++ para serem chamadas por código não excepcional, a função C++ não pode permitir que exceções se propaguem de volta para o chamador. Esses chamadores não têm como capturar ou tratar exceções C++. O programa pode ser encerrado, vazar recursos ou causar um comportamento indefinido.

Recomendamos que a função C++ extern "C" capture especificamente todas as exceções que ela sabe tratar e, se apropriado, converta a exceção em um código de erro que o chamador entenda. Se nem todas as exceções potenciais forem conhecidas, a função C++ deverá ter um bloco catch(...) como o último manipulador. Nesse caso, é melhor relatar um erro fatal ao chamador, pois seu programa pode estar em um estado desconhecido e irrecuperável.

O exemplo a seguir mostra uma função que pressupõe que qualquer exceção que possa ser gerada seja um Win32Exception ou um tipo de exceção derivado de std::exception. A função captura qualquer exceção desses tipos e propaga as informações de erro como um código de erro Win32 para o chamador.

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 você converte de exceções em códigos de erro, há um problema potencial: os códigos de erro geralmente não contêm a riqueza de informações que uma exceção pode armazenar. Para resolver esse problema, você pode fornecer um bloco catch para cada tipo de exceção específico que pode ser gerado e executar o registro em log para registrar os detalhes da exceção antes que ela seja convertida em um código de erro. Essa abordagem pode criar código repetitivo se várias funções usarem o mesmo conjunto de blocos catch. Uma boa maneira de evitar a repetição de código é refatorando esses blocos em uma função de utilitário privado que implementa os blocos trye catch e aceita um objeto de função que é invocado no bloco try. Em cada função pública, passe o código para a função de utilitário como uma expressão 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;
}

O exemplo a seguir mostra como escrever a expressão lambda que define o functor. Geralmente é mais fácil ler uma expressão lambda embutida do que código que chama um objeto de função nomeado.

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

Para saber mais sobre expressões lambda, confira o artigo sobre expressões lambda.

Chamar código excepcional por meio de código não excepcional de código excepcional

É possível, mas não recomendado, lançar exceções em código sem reconhecimento de exceção. Por exemplo, seu programa C++ pode chamar uma biblioteca que usa funções de retorno de chamada fornecidas por você. Em algumas circunstâncias, você pode gerar exceções das funções de retorno de chamada no código não excepcional que podem ser tratadas pelo chamador original. No entanto, as circunstâncias em que as exceções podem funcionar com êxito são estritas. Você precisa compilar o código da biblioteca de uma forma que preserve a semântica de desenrolamento de pilha. O código sem reconhecimento de exceção não pode fazer nada que possa prender a exceção C++. Além disso, o código de biblioteca entre o chamador e o retorno de chamada não pode alocar recursos locais. Por exemplo, o código que não tem reconhecimento de exceção não pode ter locais que apontem para memória de heap alocada. Esses recursos são vazados quando a pilha é desenrolada.

Esses requisitos precisam ser atendidos para gerar exceções no código sem reconhecimento de exceção:

  • Você pode criar todo o caminho de código no código sem reconhecimento de exceção usando /EHs,
  • Não há recursos alocados localmente que possam vazar quando a pilha for desenrolada,
  • O código não tem manipuladores de exceção estruturados __except que capturam todas as exceções.

Como gerar exceções em código não excepcional é propenso a erros e pode causar desafios difíceis com relação à depuração, não recomendamos fazer isso.

Confira também

Práticas recomendadas do C++ modernas para tratamento de erros e exceções
Como projetar tendo em vista a segurança da exceção