Excepciones, destructores y técnica RAII (IV)


Todas las excepciones emitidas por la biblioteca estándar del lenguaje C++ pertenecen a la siguiente jerarquía de clases, contenida en el espacio de nombres std y actualizada según C++20 [1]:

exception                       <exception>
     bad_alloc                  <new>
         bad_array_new_length   <new>
         bad_cast                   <typeinfo>
             bad_any_cast           <any>
     bad_exception              <exception>
     bad_function_call          <functional>
     bad_optional_access         <optional>
     bad_typeid                 <typeinfo>
     bad_variant_access         <variant>
     bad_weak_ptr               <memory>
     logic_error                <stdexcept>
         domain_error           <stdexcept>
         invalid_argument       <stdexcept>
         future_error           <future>
         length_error           <stdexcept>
         out_of_range           <stdexcept>
     runtime_error              <stdexcept>
         ambiguous_local_time   <chrono>
         format_error           <format>
         nonexistent_local_time <chrono>
         overflow_error         <stdexcept>   
         range_error            <stdexcept>
         regex_error            <regex>
         system_error           <system_error>
                 ios_base::failure  <ios>
                 filesystem::filesystem_error <filesystem> 
         underflow_error        <stdexcept>

El estándar del lenguaje define la clase base std::exception como:

   namespace std {       class exception {       public:          exception() noexcept;          exception(const exception&noexcept;          exception& operator=(const exception&noexcept;          virtual ~exception();          virtual auto what() const noexcept -> char const*;       };    }

El carácter virtual del destructor y de la función miembro what() –a redefinir por las clases derivadas, de forma que ésta retorne una cadena de caracteres con un mensaje explicatorio del error acontecido– garantizan el comportamiento polimórfico de la jerarquía.

Las excepciones de tipo std::bad_alloc se emiten ante fallos en el alojamiento de memoria [2].

Por su parte, los constructores de las subclases std::logic_error y std::runtime_error reciben como argumento el mensaje explicativo accesible a través de la función what() (véase [3, 4]):

   namespace std {       class runtime_error : public exception {       public:          explicit runtime_error(string const& mssg); // strcmp(what(), mssg.c_str()) == 0          explicit runtime_error(char const* mssg);   // strcmp(what(), mssg) == 0       };    }

Distinguimos las siguientes semánticas:
  • La subclase std::logic_error y sus clases derivadas (std::domain_error, std::out_of_range, etcétera) informan acerca de errores en principio detectables antes de la ejecución del programa, como el incumplimiento de pre-condiciones lógicas y/o invariantes de una clase. Aun cuando el estándar facilite estas excepciones y haga uso de ellas, se recomienda gestionar este tipo de situaciones a través de contratos (véase esta serie de posts para más detalles).
  • La subclase std::runtime_error y sus clases derivadas (std::system_error, std::overflow_error, etcétera) informan de errores difícilmente predecibles, debidos a eventos más allá del ámbito del programa y acontecidos durante su tiempo de ejecución (por ejemplo, como consecuencia de la inexistencia de un fichero que se desea leer).

A menudo, será necesario introducir nuevas clases de excepciones en nuestros programas distintas a las proporcionadas por la biblioteca estándar (muy en particular, cuando sea necesario ampliar la interfaz pública y la información contenida en las excepciones estándar), en cuyo caso se recomienda derivar públicamente de std::exception. El constructor copia y el operador de asignación de la nueva clase de excepción deben ser públicamente accesibles. Asimismo, no deben utilizarse en la clase otras clases base o atributos cuyos constructores copia puedan provocar, a su vez, el lanzamiento de excepciones.

En un bloque try-catch, las cláusulas catch son examinadas en el mismo orden en que son declaradas. Una vez encontrada una cláusula capaz de manejar el tipo de excepción emitida, no se examinarán las siguientes. En base a ello, el controlador de una clase derivada debe colocarse siempre antes del controlador de su clase base. Asimismo, el controlador con elipsis (...), capaz de manejar cualquier excepción, debe colocarse en último lugar. A modo de ejemplo (consúltese la lista de excepciones estándar dada al inicio del post):

   try {       auto ifs = std::ifstream{};       ifs.exceptions(std::ios_base::failbit);       // ...    }    catch (std::ios_base::failure const& e) {       std::cerr << e.what();       // ...    }    catch (std::runtime_error const&) {       // ...    }    catch (std::exception const&) {       // ...    }    catch (...) {       // capturamos cualquier otro tipo de excepción,       // realizamos algún tipo de limpieza y...       throw; // ....relanzamos la excepción    }


Referencias bibliográficas:
  1. std::exception - https://en.cppreference.com/w/cpp/error/exception
  2. std::bad_alloc - https://en.cppreference.com/w/cpp/memory/new/bad_alloc
  3. std::logic_error - https://en.cppreference.com/w/cpp/error/logic_error
  4. std::runtime_error - https://en.cppreference.com/w/cpp/error/runtime_error

Puedes acceder al siguiente artículo de la serie a través de este enlace.

No hay comentarios:

Publicar un comentario