Interfaz entre código excepcional y no excepcional

En este artículo se describe cómo implementar el control de excepciones coherente en el código de C++ y cómo traducir esas excepciones a códigos de error, y viceversa, en los límites de la excepción.

A veces, el código de C++ tiene que interactuar con código que no usa excepciones (código no excepcional). Esta interacción se conoce como límite de excepción. Por ejemplo, quizás desee llamar a la función CreateFile de Win32 en el programa de C++. CreateFile no produce excepciones. En su lugar establece los códigos de error que pueden recuperarse mediante la función GetLastError. Si el programa de C++ no es insignificante, probablemente sea preferible tener una directiva coherente de control de errores basada en excepciones. Y probablemente no sea conveniente abandonar las excepciones solo porque interactúe con código no excepcional. Tampoco le interesa mezclar directivas de error basadas en excepciones y no basadas en excepciones en el código de C++.

Llamada a funciones no excepcionales desde C++

Cuando se llama a una función sin excepciones desde C++, la idea es ajustar esa función en una función de C++ que detecte cualquier error y posiblemente inicie una excepción. Cuando diseñe una función contenedora de este tipo, decida primero qué tipo de garantía de excepción va a proporcionar: sin excepciones, fuerte o básica. En segundo lugar, diseñe la función para liberar correctamente todos los recursos, por ejemplo, los identificadores de archivo, si se produce una excepción. Normalmente, esto significa que usa punteros inteligentes o administradores de recursos similares para poseer los recursos. Para más información sobre las consideraciones de diseño, consulte Diseño para la seguridad de las excepciones.

Ejemplo

En el ejemplo siguiente se muestra que las funciones de C++ que usan internamente las funciones CreateFile y ReadFile de Win32 para abrir y leer dos archivos. La clase File es un contenedor RAII (Resource Acquisition Is Initialization) para los identificadores de archivo. Su constructor detecta una condición de "archivo no encontrado" y produce una excepción para propagar el error en la pila de llamadas del ejecutable de C++ (en este ejemplo, la función main()). Si se produce una excepción después de que un objeto File se haya construido totalmente, el destructor llama automáticamente a CloseHandle para liberar el identificador de archivo. (Si lo prefiere, puede usar la clase CHandle de Active Template Library (ATL) para este mismo propósito o unique_ptr junto con una función de eliminación personalizada). Las funciones que llaman a las API de Win32 y CRT detectan errores y, a continuación, producen excepciones de C++ mediante la función ThrowLastErrorIf definida localmente, que a su vez utiliza la clase Win32Exception, derivada de la clase runtime_error. Todas las funciones de este ejemplo proporcionan una garantía de excepción fuerte; si se produce una excepción en cualquier momento en estas funciones, no se pierden recursos y no se modifica el estado del programa.

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

Llamada a código excepcional desde código no excepcional

Los programas de C pueden llamar a las funciones de C++ que se declaran como extern "C". El código escrito en diferentes lenguajes puede usar servidores COM de C++. Al implementar funciones públicas preparadas para excepciones en C++ para que las invoque código sin excepciones, la función de C++ no debe permitir que ninguna excepción se propague de nuevo al llamador. Estos llamadores no tienen ninguna manera de detectar o controlar excepciones de C++. El programa puede finalizar, filtrar recursos o provocar un comportamiento indefinido.

Se recomienda que la función de C++ extern "C" detecte específicamente cada excepción que pueda administrar y, si es necesario, convierta la excepción en un código de error que el llamador comprenda. Si no se conocen todas las excepciones posibles, la función de C++ debe tener un bloque catch(...) como último controlador. En ese caso, es mejor notificar un error irrecuperable al llamador, porque el programa podría estar en un estado desconocido e irrecuperable.

El siguiente ejemplo muestra una función que asume que cualquier excepción que pueda producirse es una excepción Win32Exception o un tipo de excepción derivado de std::exception. La función detecta cualquier excepción de estos tipos y propaga la información de error como código de error Win32 al llamador.

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

Cuando se convierten excepciones en códigos de error, puede haber un problema: los códigos de error no contienen a menudo la riqueza de información que una excepción puede almacenar. Para resolver este problema, puede proporcionar un bloque catch para cada tipo de excepción concreto que pueda producirse. Además, puede registrarlo para anotar los detalles de la excepción antes de que se convierta en un código de error. Este enfoque puede crear código repetitivo si varias funciones usan el mismo conjunto de bloques catch. Una buena manera de evitar la repetición de código es la refactorización de esos bloques en una función de utilidad privada que implemente los bloques try y catch, y que acepte un objeto de función que se invoque en el bloque try. En cada función pública, pase el código a la función de utilidad como una expresión 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;
}

En el ejemplo siguiente se muestra cómo escribir la expresión lambda que define el objeto de función (functor). Una expresión lambda suele ser más fácil de leer insertada que el código que llama a un objeto de función con nombre.

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 más información sobre las expresiones lambda, consulte Expresiones lambda.

Llamada a código excepcional mediante código no excepcional a partir de código excepcional

Es posible, pero no se recomienda, producir excepciones en código no compatible con excepciones. Por ejemplo, el programa de C++ puede llamar a una biblioteca que use funciones de devolución de llamada que proporcione. En algunas circunstancias, puede producir excepciones de las funciones de devolución de llamada en el código no excepcional que el autor de la llamada original puede controlar. Sin embargo, las circunstancias en las que las excepciones pueden funcionar correctamente son estrictas. Debe compilar el código de biblioteca de una manera que conserve la semántica de desenredo de la pila. El código no compatible con excepciones no puede hacer nada para interceptar la excepción de C++. Además, el código de biblioteca entre el autor de la llamada y la devolución de llamada no puede asignar recursos locales. Por ejemplo, el código que no es compatible con excepciones no puede tener variables locales que apunten a la memoria de montón asignada. Estos recursos se filtran cuando la pila se desenreda.

Los siguientes requisitos deben cumplirse para producir excepciones en código no compatible con excepciones:

  • Se debe poder compilar toda la ruta de acceso del código en código no compatible con excepciones mediante /EHs.
  • No hay recursos asignados localmente que se puedan filtrar cuando se desenreda la pila.
  • El código no tiene ningún controlador de excepciones estructurado __except que detecte todas las excepciones.

Dado que producir excepciones en código no excepcional es propenso a errores y puede causar problemas de depuración difíciles de resolver, no se recomienda.

Consulte también

Procedimientos recomendados de C++ moderno para las excepciones y el control de errores
Diseño para la seguridad de las excepciones