Procedimientos recomendados de C++ moderno para el control de errores y excepciones

En C++moderno, en la mayoría de los escenarios, la manera preferida de notificar y controlar los errores lógicos y los errores en tiempo de ejecución es usar excepciones. Esto es especialmente cierto cuando la pila puede contener varias llamadas a función entre la función que detecta el error y la función que tiene el contexto para controlar el error. Las excepciones proporcionan una forma formal y bien definida para que el código que detecta errores pase la información a la pila de llamadas.

Uso de excepciones para código excepcional

Los errores de programa suelen dividirse en dos categorías:

  • Errores lógicos causados por errores de programación. Por ejemplo, un error "índice fuera del intervalo".
  • Errores en tiempo de ejecución que escapan del control del programador. Por ejemplo, un error "servicio de red no disponible".

En la programación de estilo C y en COM, los informes de errores se administran devolviendo un valor que representa un código de error o un código de estado para una función determinada, o estableciendo una variable global que el autor de la llamada puede recuperar opcionalmente después de cada llamada a función para ver si se han notificado errores. Por ejemplo, la programación COM usa el valor devuelto HRESULT para comunicar los errores al autor de la llamada. Y la API Win32 tiene la función GetLastError para recuperar el último error notificado por la pila de llamadas. En ambos casos, depende del autor de la llamada reconocer el código y responder a él adecuadamente. Si el autor de la llamada no controla explícitamente el código de error, el programa podría bloquearse sin advertencia. O bien, podría seguir ejecutándose con datos incorrectos y generar resultados incorrectos.

Las excepciones se prefieren en C++ moderno por los siguientes motivos:

  • Una excepción obliga al código que llama a reconocer una condición de error y controlarla. Las excepciones no controladas detienen la ejecución del programa.
  • Una excepción salta al punto de la pila de llamadas que puede controlar el error. Las funciones intermedias pueden dejar que la excepción se propague. No tienen que coordinarse con otras capas.
  • El mecanismo de desenredo de la pila de excepciones destruye todos los objetos del ámbito después de iniciar una excepción, según reglas bien definidas.
  • Una excepción permite una separación limpia entre el código que detecta el error y el código que lo controla.

En el ejemplo simplificado siguiente se muestra la sintaxis necesaria para iniciar y detectar excepciones en C++:

#include <stdexcept>
#include <limits>
#include <iostream>

using namespace std;

void MyFunc(int c)
{
    if (c > numeric_limits< char> ::max())
    {
        throw invalid_argument("MyFunc argument too large.");
    }
    //...
}

int main()
{
    try
    {
        MyFunc(256); //cause an exception to throw
    }

    catch (invalid_argument& e)
    {
        cerr << e.what() << endl;
        return -1;
    }
    //...
    return 0;
}

Las excepciones en C++ se asemejan a las de lenguajes como C# y Java. En el bloque try, si se produce una excepción, la detectará el primer bloque catch asociado cuyo tipo coincida con el de la excepción. En otras palabras, la ejecución salta de la instrucción throw a la instrucción catch. Si no se encuentra ningún bloque catch utilizable, se invoca std::terminate y se cierra el programa. En C++, se puede producir cualquier tipo; sin embargo, se recomienda iniciar un tipo que derive directa o indirectamente de std::exception. En el ejemplo anterior, el tipo de excepción, invalid_argument, se define en la biblioteca estándar del archivo de encabezado <stdexcept>. C++ no proporciona ni requiere un bloque finally para asegurarse de que todos los recursos se liberan si se produce una excepción. La adquisición de recursos es la expresión de inicialización (RAII), que usa punteros inteligentes, que proporciona la funcionalidad necesaria para la limpieza de recursos. Para más información, consulte Diseño de la seguridad de excepciones. Para información sobre el mecanismo de desenredo de pila de C++, consulte Excepciones y desenredo de pila.

Directrices básicas

El control sólido de errores es complicado en cualquier lenguaje de programación. Aunque las excepciones proporcionan varias características que permiten un buen control de errores, no pueden hacer todo el trabajo por usted. Para aprovechar las ventajas del mecanismo de excepciones, tenga en cuenta las excepciones al diseñar su código.

  • Use aserciones para comprobar si hay condiciones que siempre deban ser verdaderas o siempre falsas. Use excepciones para comprobar si hay errores que pueden producirse, por ejemplo, errores en la validación de entrada en parámetros de funciones públicas. Para más información, consulte la sección Excepciones frente a aserciones.
  • Use excepciones cuando el código que controla el error esté separado del código que detecta el error mediante una o varias llamadas a función intermedias. Considere si usar códigos de error en su lugar en bucles críticos para el rendimiento, cuando el código que controla el error esté estrechamente acoplado al código que lo detecta.
  • Para cada función que pueda producir o propagar una excepción, proporcione una de las tres garantías de excepción: la garantía fuerte, la garantía básica o la garantía de que no haya excepciones (noexcept). Para más información, consulte Diseño de la seguridad de excepciones.
  • Inicie excepciones por valor y detéctelas por referencia. No detecte lo que no pueda controlar.
  • No use especificaciones de excepciones, que están en desuso en C++11. Para más información, consulte la sección Especificaciones de excepciones y noexcept.
  • Use los tipos de excepción de biblioteca estándar cuando se apliquen. Derive tipos de excepción personalizados de la jerarquía exception Class.
  • No permita que las excepciones escapen de destructores o funciones de desasignación de memoria.

Excepciones y rendimiento

El mecanismo de excepción tiene un costo de rendimiento mínimo si no se produce ninguna excepción. Si se produce una excepción, el costo del recorrido y el desenredo de pila es comparable aproximadamente al costo de una llamada a función. Se requieren otras estructuras de datos para realizar un seguimiento de la pila de llamadas después de especificar un bloque try, y se requieren más instrucciones para desenredar la pila si se produce una excepción. Sin embargo, en la mayoría de los escenarios, el costo en el rendimiento y la superficie de memoria no es apreciable. Es probable que el efecto adverso de las excepciones en el rendimiento sea apreciable solo en sistemas con limitaciones de memoria. O bien, en bucles críticos para el rendimiento, donde es probable que se produzca un error con regularidad y haya un acoplamiento estricto entre el código para controlarlo y el código que lo notifica. En cualquier caso, es imposible conocer el costo real de las excepciones sin generar perfiles y medir. Incluso en esos casos poco frecuentes en que el costo es significativo, puede sopesarlo con respecto al aumento de la exactitud, la facilidad de mantenimiento y otras ventajas proporcionadas por una directiva de excepciones bien diseñada.

Excepciones frente a aserciones

Las excepciones y las aserciones son dos mecanismos distintos para detectar errores en tiempo de ejecución en un programa. Use instrucciones assert para probar las condiciones durante el desarrollo de que siempre deben ser verdaderas o siempre falsas si todo el código es correcto. No tiene sentido controlar un error de este tipo utilizando una excepción, porque el error indica que hay que arreglar algo en el código. No representa una condición de que el programa tenga que recuperarse en tiempo de ejecución. Un elemento assert detiene la ejecución en la instrucción para que pueda inspeccionar el estado del programa en el depurador. Una excepción continúa la ejecución desde el primer controlador catch adecuado. Use excepciones para comprobar las condiciones de error que pueden producirse en tiempo de ejecución incluso si el código es correcto, por ejemplo, "archivo no encontrado" o "memoria insuficiente". Las excepciones pueden controlar estas condiciones, incluso si la recuperación simplemente envía un mensaje a un registro y finaliza el programa. Compruebe siempre los argumentos de las funciones públicas mediante excepciones. Incluso si la función no tiene errores, es posible que no tenga control total sobre los argumentos que un usuario podría pasar a ella.

Excepciones de C++ frente a excepciones de SEH de Windows

Los programas de C y C++ pueden usar el mecanismo de control de excepciones estructurado (SEH) en el sistema operativo Windows. Los conceptos de SEH se asemejan a los de las excepciones de C++, excepto que SEH usa las construcciones __try, __except y __finally en lugar de try y catch. En el compilador de Microsoft C++ (MSVC), se implementan excepciones de C++ para SEH. Sin embargo, al escribir código de C++, use la sintaxis de excepciones de C++.

Para más información, consulte Control de excepciones estructurado (C/C++).

Especificaciones de excepciones y noexcept

Las especificaciones de excepciones se introdujeron en C++ como una manera de especificar las excepciones que podría producir una función. Sin embargo, las especificaciones de excepciones demostraron problemas en la práctica y están en desuso en el borrador estándar de C++11. No se recomienda usar especificaciones de excepciones throw, salvo para throw(), que indica que la función no permite escapar excepciones. Si debe usar especificaciones de excepciones del formato en desuso throw( type-name ), la compatibilidad con MSVC es limitada. Para más información, consulte Especificaciones de excepciones (throw). El especificador noexcept se introduce en C++11 como alternativa preferida a throw().

Consulte también

Interfaz entre código excepcional y no excepcional
Referencia del lenguaje C++
Biblioteca estándar de C++