Excepciones
Excepciones
Una excepción puede definirse como la ocurrencia de un error en tiempo de ejecución
Al hecho de mostrar la condición de excepción, al que invocó la operación que causó dicha excepción, se le denomina generar (señalar o lanzar) la
excepción, mientras que, a la respuesta del invocante
(directo o indirecto) se le denomina gestión, manejo
o captura de dicha excepción
La gestión de excepciones se puede considerar un mecanismo de recuperación de errores hacia
adelante aunque, puede utilizarse también para
Excepciones
En la práctica, la mayor parte de un sistema, está dedicada a tratar con situaciones anormales,
excepcionales o no deseadas;
la parte más pequeña corresponde a la propia aplicación:
más de 2/3 del código! está dedicada a la detección y
manejo de errores (Cristian, 1982)
Es muy probable que esta porción, contenga errores ya que, además de su complejidad,
Puesto que rara vez se ejecutan, es el general la parte del
código menos ensayada, documentada y entendida
La mayoría de las fallas de diseño existente en un sistema parecen estar ubicadas en el código que maneja situaciones excepcionales
Excepciones
El ejemplo más citado en la literatura es
el accidente del cohete
Ariane 5
debido a
una excepción de software
no manejada
.
25 años después de Cristian (Cabral y
Marques) …gracias a Java
En Exception Handling: A Field Study in Java and .NET Menos del 10% del código se dedica al tratamiento de errores y, el porcentaje más común está alrededor de un 5%
Los desarrolladores rechazan usar este mecanismo,
escribir código de calidad para manejo de errores es una tarea compleja, engorrosa y propensa a errores. Se
Potencial incremento en la
complejidad en Java
Potencial incremento en la
complejidad en Java
Cuidado con su uso! Debería volverse a las raíces y recordar que las excepciones son excepcionales, usarlas para eventos raros (que no ocurren
frecuentemente), no para el control del flujo normal. Lamentablemente no está de acuerdo con el uso
actual de las excepciones en lenguajes tales como Java.
En el siguiente ejemplo el 1º método es 750 veces más lento que el 2º (bajo Windows XP y Java 1.4) si se llama con una referencia que no sea del tipo
Potencial incremento en la
complejidad en Java
public static boolean testForInteger1(Object x) {
try {
Integer i = (Integer) x;
return true;}
catch (Exception e) {
return false;}}
public static boolean testForInteger2(Object x)
{return x instanceof Integer;}
En C++
En C++ se necesita prestar atención a las excepciones
sólo en aquéllos lugares donde se violan invariantes de clases, en todo el resto del código no hay que
preocuparse
Las invariantes de clase
describen condiciones que son válidas para todos los objetos de dicha clase
garantizan que un objeto está en un estado válido
Las implementaciones C++ modernas reducen la sobrecarga del uso de excepciones a un porcentaje pequeño (un 3%, otros afirman entre un 10-15% y algunos cerca de 30%)
Excepciones
Aunque el trabajo de
Goodenough
(
1975
)
puso los cimientos de la
terminología
relacionada con las excepciones
, aún hoy día,
no existe un acuerdo
en la
definición del
concepto de excepción, como así tampoco en
cuanto a su utilización
En realidad, la palabra excepción sugiere
“
algo que ocurre muy raramente
”. Sin
embargo, esto se condice sólo parcialmente,
con el uso que comúnmente se hace de ellas
en la práctica diaria de la programación
Excepciones
Las excepciones constituyen una forma adicional de pasar información al invocante de un método
Por tanto, resuelven el problema de los lenguajes de programación que permiten un único valor de retorno Pueden utilizarse para distintos propósitos. Goodenough (1975) señala 3:
Señalar una avería
Clasificar un resultado (Ej.: overflow en suma, EOF),
información adicional del resultado para que el invocante pueda interpretarlo adecuadamente
Control o monitoreo (Ej. “se han procesado n registros”), el
invocante desea que se le notifique que se alcanzó alguna condición
Por tanto, no necesariamente indican “eventos
excepcionales”. Ej.: en Java InterruptedException se utiliza para sincronizar threads
¿Qué es excepcional?
No poder cumplir una precondición en una función
Un constructor no puede construir un objeto (no puede establecer las invariantes de la clase)
Un error fuera de rango v[v.size()] = 7
Incapacidad de adquirir un recurso (ej. Se cae una red)
En contraste, la terminación de un lazo ordinario no es excepcional, a menos que sea infinito la terminación es normal y esperada
¿Qué es excepcional?
No lance excepciones como forma alternativa de retornar un valor de una función o, donde bastaría usar estructuras de control locales
Excepciones en C++
Para un efectivo manejo de errores usando
excepciones, los mecanismos del lenguaje deben usarse basándose en una estrategia.
Por tanto presentaremos los conceptos de garantía de seguridad frente a excepciones, central para la
recuperación de errores en tiempo de ejecución y, la técnica RAII (Resource Adquisition is Initialization) para la gestión de recursos usando constructores y
destructores.
Excepciones
La noción de una excepción se provee para ayudar a suministrar información desde el punto donde se
detecta el error hasta el punto donde pueda manejarse.
Idea general para manejar errores en forma no local
Una función que no puede manejar un problema lanza (throws) una excepción, esperando que su invocante
(directo o indirecto) pueda manejarlo
La función, en la cadena de invocaciones, que desee manejar un determinado problema, lo indica atrapando (catching) la correspondiente excepción
Excepciones
Un componente invocante indica el tipo de fallas que está dispuesto a manejar especificando esas excepciones en las cláusulas catch de un bloque try
Un componente invocado que no puede completar su tarea asignada, informa este hecho lanzando una
excepción usando una expresión throw
Invocado Invocante
Excepciones
La función taskmaster() está preparada para manejar una excepción del tipo Some_error pero, pueden
generarse otros tipos de excepciones.
Por ejemplo, do_task() puede llamar a otras
funciones para hacer varias sub tareas, algunas de las cuales pueden generar excepciones debido a que no pueden ejecutar sus sub tareas asignadas.
Una excepción diferente de Some_error indica que
taskmaster() falló en hacer su tarea asignada y, esta falla debe manejarse por alguna función que invoque
Excepciones
Una función invocada no puede sólo retornar un
indicativo que ocurrió un error.
Si el programa continúa trabajando (y, no sólo imprime un mensaje de error y termina), el retorno de la función invocada debe
dejar el programa en un buen estado Y, no perder recursos
En C++ el mecanismo de manejo de excepciones está integrado con aquéllos del constructor/destructor y
Mecanismo de gestión de
excepciones
Alternativa a los mecanismos tradicionales cuando son insuficientes, poco elegantes y propensos a errores
Es completo, puede usarse para manejar todos los errores detectados por código ordinario
Permite al programador separar explícitamente el código de manejo de errores del código “normal”, haciendo el programa más legible
Permite un estilo de más regular de manejo de errores, lo que simplifica la cooperación entre fragmentos de códigos escritos separadamente
Excepciones
Una excepción es un objeto lanzado (thrown) para representar la ocurrencia de un error
Puede ser de cualquier tipo que pueda copiarse pero, se recomienda usar sólo tipos definidos por el usuario,
específicamente para este propósito.
De esa forma, minimizamos las posibilidades que 2 librerías no relacionadas usen el mismo valor (por
ejemplo 17) para representar distintos errores, lo cual lleva el código de recuperación de errores al caos.
Excepciones
Si eso se vuelve tedioso, la librería estándar tiene una pequeña jerarquía de clases de excepción
Una excepción puede contener información sobre el error que representa.
Su tipo representa la clase de error y cualquier dato que contenga representa la ocurrencia particular de ese
error. En la librería estándar contiene un string que puede usarse para transmitir información de la
Manejo de errores tradicional
Consideremos las alternativas al manejo de
excepciones para una función que detecta un problema
que no puede manejar localmente, de forma tal que el error debe reportarse al invocante de dicha función. Cada enfoque tradicional tiene problemas y, ninguno es general
Tradicionalmente en una aplicación coexisten una combinación no sistemática de ellas
Caos en programas compuestos de partes desarrolladas por
Terminar el programa
Enfoque muy drástico.
Para la mayoría de los errores se puede y debe hacerse algo mejor
Por ejemplo, en la mayoría de las situaciones deberíamos por lo
menos escribir un mensaje decente o registrar el error antes de terminar.
En particular, una librería que no conoce el propósito o estrategia general del programa en la cual estará
embebida, no puede simplemente exit() o abort().
Una librería que termina incondicionalmente no puede
Retornar un valor de error
Esto no siempre es factible puesto que a menudo no hay valores de error aceptables
Para esta función, cada resultado entero es un valor
posible, no hay ningún valor entero para representar una entrada fallida.
Como mínimo deberíamos modificar la función para que retorne un par de valores.
Aún cuando este enfoque sea factible, generalmente es
inconveniente debido a que, en cada invocación debe
chequearse para ver si se produjo un error.
Retornar un valor de error
Además, a menudo los invocantes ignoran la
posibilidad de errores o simplemente olvidan testear uno de los valores retornados.
Por ejemplo, printf retorna un valor negativo si ocurrió un
error de salida o codificación, aunque los programadores esencialmente nunca verifican esto.
Finalmente, algunas funciones no tienen valores de retorno, un ejemplo obvio son los constructores
Retornar un valor legal y dejar el
programa en estado erróneo
Tiene el problema que la función invocante puede no notar que el programa quedó en un estado erróneo. Por ejemplo, muchas funciones de la librería estándar de C (las funciones matemáticas no lo requieren)
Retornar un valor legal y dejar el
programa en estado erróneo
Aquí el valor de d no tiene sentido y errno se setea
(en 0 antes de invocar) para indicar que -1.0 no es un valor aceptable para esta función (NaN dependiendo de la implementación, errno distinto de 0).
Sin embargo, los programas fallan de configurar y testear errno para evitar errores consecuentes
causados por valores retornados en invocaciones fallidas.
Además, el uso de variables no locales para registrar condiciones de error, no funciona bien en presencia de
Invocar una función manejadora
de error
Es alguno de los anteriores pero disfrazado puesto que, el problema se vuelve inmediatamente: ¿Qué hace esta
función?
A menos que dicha función pueda resolver
completamente el problema (en este caso ¿porqué lo consideraríamos un error?), debe a su vez,
terminar el programa,
retornar con alguna indicación que ocurrió un error, establecer un estado de error o,
Excepciones
El manejo de errores seguirá siendo una tarea difícil
El manejo de excepciones, aunque mas formal que las técnicas a las que reemplaza, es aún relativamente
desestructurada, en comparación con otras
características del lenguaje que sólo involucran el flujo de control local.
El mecanismo de manejo de excepciones de C++ provee al programador de una forma de manejar errores donde ellos puedan ser manejados más naturalmente
Las excepciones hacen visible la complejidad del
manejo de errores. Sin embargo no son la causa de aquella complejidad
¿Cuándo no usarlas?
Un componente de un sistema embebido con restricciones
temporales críticas, donde se debe garantizar que cumpla con sus
deadlines. En ausencia de herramientas que puedan estimar con precisión el tiempo máximo para que una excepción se propague de un throw a un catch, deben usarse métodos de manejo de errores alternativos
Un programa viejo y grande en el que la gestión de memoria se
realiza ad hoc usando new y delete, en lugar de usar algún esquema sistemático tales como manejadores de recursos (ej. Usen vector, string).
En sistemas con poca capacidad de memoria y donde el soporte para el manejo de excepciones consuma unos 2K de memoria Ídem que antes, recurrir a las técnicas tradicionales.
Artículo interesante
En “Abnormal Events Handling for Dependable
Embedded Systems” (2006), Luis E. Leyva-del-foyo y otros
Se analizan las dificultades de manejar eventos anormales e, introducen un marco de trabajo que integran conceptos de:
Diseño por contrato,
niveles de seguridad frente a excepciones (imitado en C) y, las distintas fases de tolerancia a fallas.
Diseñan un mecanismo que utiliza códigos de errores, excepciones y aserciones ejecutables en código de
Cláusula throw
La cláusula throw lanza una excepción. En la cláusula puede ir un int, un float, un string o un objeto de cualquier tipo que pueda copiarse. Se parece a un return (return 30, …throw 30), ambos informan el
resultado al invocar una función. Return tiene un tipo fijo, throw puede lanzar cualquier tipo
Cláusula throw
En general no se recomienda usar tipos primitivos en la claúsula throw, tal como int.
Es preferible definir tipos con el único fin de manejar excepciones, lo cual minimiza la confusión acerca de su propósito.
El objeto excepción atrapado, es en principio, una
Cláusula throw
Esta variable temporal puede copiarse varias veces antes de ser atrapada: la excepción se pasa de la función invocada a las invocantes, así hasta que se encuentra un manejador apropiado.
Por tanto nunca poner demasiada cantidad de datos en las excepciones
Los datos asociados al objeto excepción (si existen) se usan típicamente para producir mensajes de error
o para ayudar en la recuperación.
El tipo de excepción se usa para seleccionar el manejador (catch de un bloque try)
Rica información en las
excepciones
Propagación de excepciones
Propagación de excepciones
El proceso de pasar una excepción a través de la cadena de invocaciones, desde el punto donde se
detectó hasta un manejador se llama “stack unwinding”. Si ninguna función en la cadena de invocaciones tiene un manejador adecuado, la excepción es manejada por el runtime, abortando el programa. En C++ se invoca a
Propagación de excepciones
El tiempo de vida de los objetos locales de la función donde se generó la excepción, termina
Propagación de excepciones
Los destructores de los objetos construidos en el bloque try que falló, se invocan en orden inverso al de la construcción
En el ejemplo se destruyen los strings, luego del lanzamiento en h() en este orden: “not”, “or”, “excess”, “in”
aunque no “at all” ya que nunca se ejecutará ni tampoco “Byron” ya no fue afectado
Terminación anormal
std::terminate() (invoca a std::abort()) hace que se imprima un mensaje de error indicando el tipo de excepción que ocurrió
Terminación anormal
Las reglas específicas para invocar a terminate() son, entre otras:
Cuando no se halla un manejador apropiado para una excepción que se lanzó
Cuando una función noexcept trata de salir con un throw
Cuando se invoca un destructor durante el desapilado y éste trata de salir con un throw
Cuando el código invocado al propagar una excepción (por ejemplo, un constructor de copia) trata de salir con un throw
Terminación anormal
Un usuario puede invocar terminate() si no son factibles enfoques menos drásticos.
Por defecto terminate() llama abort() que es la opción correcta para la mayoría de los usuarios, especialmente durante el debug.
Sino es aceptable el usuario puede proveer una función manejadora de la terminación invocando
Terminación personalizada
Por suerte el programa puede ganar el control
reemplazando la llamada a abort() por la invocación a la función callback que le pasemos como
Terminación personalizada
Hay 4 reglas que se aplican a dicha función:
No debe tomar argumentos
No debe retornar, sólo puede terminar su ejecución con exit
o abort
No está permitido que lance una excepción
Debe añadir el header <exception> para la función set_terminate()
Igualmente, no es una forma muy efectiva de manejar excepciones
Atrapando excepciones
El manejador H se invoca si:
1. H es del mismo tipo que E
2. H es una clase base pública de E
3. Si H y E son de tipo punteros y 1 o 2 se mantienen para
los tipos a los que se refieren
4. H es una referencia y 1 o 2 se mantiene para el tipo al cual
Cláusula throw
Aconsejado throw por valor (guarda en variable temporal) y atrape por referencia o referencia
Cláusula throw
Agrupamiento de excepciones
Se puede utilizar las jerarquías de herencia para crear familias de excepciones para reflejar las
relaciones entre las distintas clases de errores que representan
Cláusula throw
Atrapar por referencia en lugar de por copia (al igual que cuando se pasan argumentos por copia a una función) evita el slicing
La mayoría de los manejadores (catch) no modifican la excepción atrapada y, en general, se recomienda el uso de const, que además enfatiza que esto no ocurrirá en el bloque catch
Slicing
Al igual que cuando se pasan parámetros por valor, cuando se atrapa por valor puede producirse «slicing»
Orden en los catch
Debido a que una excepción derivada puede ser
atrapada por un manejador para más de un tipo de
excepción, es importante el orden en el cual son escritos los manejadores
Los manejadores son procesados en el orden en el que
Atrapando excepciones
Minimice el uso explícito de try/catch, es verboso y su uso no trivial es propenso a errores.
try/catch puede ser un signo de un manejo de recursos o de errores no sistemático y/o de bajo nivel
Atrape las excepciones sólo cuando vaya a hacer algo significativo con ellas (implican complejidad y gasto),
tampoco lo haga sólo para lanzar una diferente.
En caso contrario, deje que se propaguen hasta el nivel superior, allí informe sobre la falla y continue o exit
Permita que las acciones de limpieza (liberación de recursos adquiridos) sean manejadas por RAII
C++11: noexcept
Usado para indicar que una función no lanzará
excepciones, no hace falta usarlo en cada función
Si igualmente lo hace, se invoca a std::terminate (no se invocarán destructores para los objetos locales)
Muchas funciones de la librería estándar son noexcept y, lo son todas las heredadas de la librería estándar de C
Pueden declarar a una función condicionalmente
noexcept. En la librería estándar esto es muy común, e importante en operaciones que se aplican a contenedores
C++11: noexcept
Los destructores son noexcept por defecto (C+11) (no deberían lanzar excepciones)
Cuando se aplica a una función, se le permite al
compilador generar mejor código objeto (establece ciertas optimizaciones)
También es una forma de informar a los programadores acerca de la función declarada noexcept y, ver si es
necesario o no, tratar las excepciones que puedan (o no) generarse al invocar dicha función
double compute(double d) noexcept { return log(sqrt(d <= 0 ? 1 : d)); }
Catch all en C++
Técnica subóptima puesto que maneja todas las excepciones de la misma forma (debería estar en
último lugar en la lista de catch de un try), no provee información detallada de la excepción ocurrida
Catch all en C++
Puede proveerse como “último recurso” para que el programa termine de forma consistente en el caso de
excepciones no esperadas No permite tomar acciones
significativas de
recuperación más que relanzar la excepción que ocurrió (throw;)
(ineficiente, código difícil de mantener)
Catch all en C++
Debería usarse sólo en main o cuando se invoca
código de terceros que no se sabe cómo manejaron las excepciones
Rethrow la misma excepción
Un rethrow es indicado con un throw sin un operando
Útil para manejar problemas locales y pasar la excepción al invocante para acciones necesarias
posteriores donde sea más apropiado, en caso que el
manejador no pueda manejar completamente el error
Esto es válido sólo dentro de un catch o, desde dentro de un catch de una función invocada directa o
indirectamente (común en un catch all, previo tareas de limpieza)
Rethrow
A veces, la información necesaria para manejar mejor el error no está disponible en un solo lugar, de forma tal que, la acción de recuperación está mejor
distribuida sobre varios manejadores
Si se intenta un rethrow donde no hay una excepción para relanzar se invoca std::terminate().
¿Porqué C++ no tiene
finally
?
Porque tiene destructores que se encargan de liberar recursos
Jerarquía de excepciones estándar
en C++
Jerarquía de excepciones en C++
En la librería estándar hay una pequeña jerarquía de excepciones que puede usarse directamente, para
excepciones que requieran un manejo genérico o, como clases bases
Las 2 clases principales de excepciones estándar logic_error
Destinado a errores previsibles
Argumentos de funciones no válidos, invariantes violados, etc. Una alternativa potencial a assert
run_time error
Clase Exception
Clase base de todas las excepciones estándar, definido en el header <exception>
what() retorna un mensaje dependiente de la
Usando las excepciones estándar
Están disponibles para usar en los programa
Se pueden usar como están, generar subclases de ellas o
ignorarlas y crear excepciones propias
Invariantes
Para recuperarse de un error, un programa debe quedar en un “buen estado” (válido, consistente, legal)
Cada clase tiene una noción sobre qué significa “buen estado”, dado por sus invariantes (Hoare, 1972)
Una invariante es establecida en el constructor de una clase. Java, Python, C#, no pueden implementar
invariantes por construcción
Condición lógica para los datos miembro que, el constructor debe establecer y que,
las funciones miembro, que acceden a la representación del estado de un objeto hasta que se destruyen, deben
asumirlas (son las pre y postcondiciones)
Invariantes
En la siguiente clase Vector tiene una invariante simple que es:
v apunta a un arreglo de sz elementos enteros y, Todas las funciones miembros están escritas con la
suposición que la misma es verdadera o sea que, esta regla simple impuesta por el diseñador, debe mantenerse siempre que se invoca una función miembro y,
debe asegurarse al retornar de las mismas que dicha
Invariantes
Por ejemplo, en size() no se cambia ningún dato miembro, o sea se mantienen las invariantes que garantizan que sz realmente almacena el número de elementos
El operador suscripto es más comprometido aunque, es simple porque está escrito basado en las
invariantes básicas (no hay que chequear v!=0), aunque ¿cómo se basa en ellas?
Invariantes
Si el constructor se definió de la siguiente forma, si new genera una excepción y no se construye ningún objeto, es imposible crear un Vector que mantenga los elementos requeridos
Para no perder recursos, el destructor debería liberar la memoria adquirida por el constructor.
La razón por la cual es tan simple, es porque confía en la invariante de que v está apuntando a memoria asignada
Invariantes
Considere la siguiente implementación ingenua del operador de asignación.
¿Puede generar excepciones?
Si eso ocurre, ¿Siguen manteniéndose las invariantes?
En realidad, es un desastre esperando suceder!
Veamos un ejemplo donde pueda comprobarse esta afirmación
Invariantes
Si espera, al ejecutarlo, que aparezca el mensaje
Oops: memory exhausted! debido a que no tiene 320 Mb de memoria dinámica de sobra, se sentirá
decepcionado.
Sino tiene unos 160 Mb libres en el heap, la
construcción de v2 fallará de manera controlada y producirá dicho mensaje de error pero,
Sin embargo, si tiene esa cantidad de heap libre pero no los 320 Mb, eso no sucederá
Cuando la operación de asignación trate de otorgar memoria para la copia de los elementos (v=new int[sz]), se generará una excepción bad_alloc.
Invariantes
Cuando se genera la excepción, al salir del bloque try donde se intenta definir vec de capacidad 320 Mb, se invoca al destructor que intenta desasignar el espacio de vec.v pero,
el operador = ya había hecho un delete de dicho espacio!
Algunos gestores de memoria desaprueban estos
intentos de hacer un delete 2 veces en el mismo área del heap, algunos entran en un lazo infinito
Invariantes
El error está en que el operador = falló de mantener la invariante: v apunta a un array de sz enteros,
hecho esto era cuestión de tiempo para que ocurra un desastre
Arreglar este problema es fácil: Debe asegurarse que la invariante se mantenga antes de lanzar una
excepción. O, aún más simple, no deseche una buena representación antes de tener una alternativa.
Invariantes
Ahora si new falla al reservar memoria (para p) y lanza una excepción, el vector a ser asignado (lado izquierdo del =) sigue sin modificarse y,
en particular, en nuestro ejemplo si esto ocurre, al lanzarse la excepción termina mostrando el mensaje
Oops: memory exhausted!.
El código dentro de una función miembro puede romper las invariantes de clase mientras que sean restauradas antes que retorne (postcondición)
Ahora Vector es un ejemplo de manejo de recursos
(los elementos del array) de forma simple y segura usando la técnica RAII
Manejo de recursos
Los recursos no son solamente “memoria” y, al igual que con ella, requieren una operación de liberación
luego de adquirirlos y usarlos
Ejemplos:
Conexión a bases de datos remotas
Apertura y cierre de archivos/sockets/windows Trabajo seguro con mutex
Adquisición y liberación de memoria dinámica desde el heap Conexiones de red, threads
RAII
Técnica general que se basa en las propiedades de
constructores y destructores y su interacción con el mecanismo de manejo de excepciones
Cuando una función adquiere un recurso (Ejemplo: abrir un archivo, asignar memoria dinámica, adquirir un mutex, etc),
en general, es esencial para la ejecución futura del
sistema que, el recurso sea liberado apropiadamente; a menudo, esto debe concretarse antes de retornar a su invocante
RAII
La simetría de constructor/destructor refleja la simetría inherente en los pares de funciones para
adquirir/liberar recursos tales como fopen/fclose, lock/unlock, new/delete, etc.
RAII
Esto parece plausible hasta que, se realiza algo que falla luego de invocar a fopen() y, antes de invocar a fclose(). Una excepción puede provocar que
use_file() termine sin invocar a fclose()
RAII
El código donde se usa el archivo se encierra en un
bloque try que atrapa cada excepción, cierra el archivo y
relanza la excepción. Esta solución es muy verbosa,
tediosa y potencialmente costosa. Peor aún, este código se vuelve potencialmente mas complejo cuando hay que adquirir y liberar varios recursos. La forma general del problema es:
Afortunadamente hay una
RAII
Típicamente es importante liberar recursos en orden inverso a la adquisición. Liberar correctamente los recursos es particularmente importante para
programas que se ejecutan durante largo tiempo
Esto claramente se asemeja al comportamiento de los objetos locales creados por constructores y
destruidos por destructores cuando salen fuera de su ámbito.
Por lo tanto podemos manejar la adquisición y
liberación de tales recursos usando objetos locales de clases con constructores y destructores. Por ejemplo podemos crear una clase File_ptr (clase
RAII
Podemos construir un objeto File_ptr dando un FILE* o, los argumentos requeridos por fopen(). En
cualquier caso, un objeto File_ptr será destruido cuando sale de su ámbito y su destructor cerrará el archivo.
File_ptr lanza una excepción sino puede abrir un archivo porque, de lo contrario cada operación que use el archivo tendrá que chequear por nullptr.
Nuestra función use_file ahora se reduce a esta mínima:
RAII
Si el constructor no puede construir un objeto válido debe lanzar una excepción. Dejar un objeto en
RAII
El destructor será invocado independientemente, si la función termina normalmente o, si sale debido a que se señaló una excepción.
El mecanismo de manejo de excepciones nos
permite remover el código de manejo de errores del algoritmo principal.
El código resultante es más simple y menos propenso a errores que su tradicional contrapartida.
RAII
La adquisición de un recurso (aquí FILE*) está ligada a la
construcción (inicialización) de un objeto mientras que, la
liberación lo está a la destrucción del mismo
El compilador asegura que el destructor será invocado cada vez que una variable local (pila) deja su ámbito (aún debido a una excepción)
Los recursos críticos serán delimitados a objetos locales
RAII es crítico para escribir código seguro frente a excepciones
Para todos los recursos
Memoria (hecho por std::string, std::vector, std::map….)
Locks (std::unique_lock), archivos (std::fstream), sockets, threads
(std::thread)…, punteros inteligentes (gestión de memoria dinámica)
RAII
A menudo, se sugiere que escribir una clase handle
(RAII) es tedioso por lo que, proveer una sintaxis para la acción catch(…) sería una mejor solución. El problema con este enfoque se necesita recordar “atrapar y corregir” el problema, dondequiera que se adquiera un recurso en forma no disciplinada
(normalmente decenas o centenas de lugares en un programa grande), mientras que, la clase handle de RAII necesita escribirse una sola vez
Ejemplo
Del libro “Embedded programming with modern C++ Cookbook - Practical recipes to help you build robust and secure embedded applications on Linux” de Igor Viarheichyk (2020)
Ejemplo
Podemos ver un claro orden (ningún thread worker es interrumpido por otro y cada uno se ejecuta del principio al fin)
lock_guard es un wraper encima de un mutex que usa la técnica RAII que automáticamente hace un lock del mutex en el constructor y hace
automáticamente un unlock en el destructor del
mismo, cuando el objeto envuelto sale de su alcance Aquí se usa para proteger la sección crítica del código (código entre paréntesis en la función worker, estos paréntesis definen el ámbito de dicha sección crítica)
RAII
Un objeto no se considera construido hasta que su
constructor se completa. Entonces y, sólo entonces, el “desapilado” invocará al destructor de dicho objeto. Un objeto construido de sub objetos se construye en la medida que se construyen sus sub objetos. Un array se construye en la medida que se construyen sus elementos (sólo los elementos construidos completamente son
destruidos en el desapilado)
Un constructor intenta asegurarse que sus objetos se construyan completa y correctamente. Cuando esto no
puede alcanzarse, un constructor bien escrito, restaura en la medida de lo posible el estado del sistema a aquél de antes de la creación; no deja sus objetos “a medio
RAII
Considere una clase en la cual necesita adquirir 2 recursos: un archivo y un mutex, esta adquisición puede fallar y lanzar una excepción.
El constructor nunca debe completarse habiendo
adquirido 1 de los 2 recursos solamente (o sin poder adquirir ninguno).
Esto debería lograrse sin imponer una sobrecarga de complejidad para el programador.
En el siguiente ejemplo, en una clase X se adquieren 2 recursos: objetos de las clases File_ptr y
std::unique_lock. La adquisición de ambos recursos,
está representada por la inicialización de ambos objetos locales que representan los recursos
RAII
Aquí si ocurre una excepción después que p sea construido pero, antes que lck sea construido,
entonces el destructor de p (pero no el de lck ya que no se llegó a construir) será invocado.
El autor del constructor no necesita escribir código explícito para manejo de excepciones
RAII
El recurso más común es la memoria: string, vector y otros contenedores de la librería estándar usan RAII
para manejar implícitamente la adquisición y liberación.
Comparado con el manejo ad-hoc de memoria
usando new/delete, ahorra un montón de trabajo y evita un montón de errores.
En lugar de objetos locales si necesita punteros a objetos, use los declarados en la librería estándar
tales como unique_ptr y shared_ptr para evitar fugas de memoria
RAII
Provee una estrategia más elegante a las usadas en
Java, etc.
“Destruction Is Resource Release (DIRR)”, tal vez sea el nombre más apropiado
En caso de trabajar con un hardware excesivamente limitado o en STR duros (excepciones no
suficientemente predecibles desde el punto de vista de tiempo) y, no pueda usar excepciones: simule RAII,
chequee que los objetos sean válidos luego de su
construcción y que se puedan liberar todos los recursos en los destructores.
RAII es la mejor y más sistemática forma de manejar recursos, aún sin excepciones
Ejemplo
Si se desea escribir
Si g no fue construido correctamente, func termina con una excepción, sino se puede lanzar una excepción,
puede simular el estilo RAII, añadiendo a Gadget una función miembro valid(). El problema ahora es que el invocante tiene que recordar para testear el valor de retorno
Ver orden de liberación
https://www.modernescpp.com/index.php/garbage-collectio-no-thanks
Wrapper para FreeRTOS
Michael Becker implementó una serie de clases wrapers para encapsular funcionalidades de FreeRTOS:
https://github.com/michaelbecker/freertos-addons,
permitiendo escribir código para una aplicación en C++ a la vez que use FreeRTOS. Presenta además 48 demos de proyectos mostrando cómo usar su librería
En
https://github.com/michaelbecker/freertos-addons/blob/master/c%2B%2B/Source/include/mute
x.hpp puede ver la clase LockGuard que se comporta
Reglas generales
Un objeto es completamente construido cuando termina su constructor
Si ello no ocurre, un constructor compatible con RAII,
deja al sistema con tan pocos cambios como sea posible Si un objeto está compuesto de sub-objetos es
construido hasta que todas sus partes sean construidas Si se sale de un ámbito (bloque, función…), entonces se invocan los destructores de todos los objetos locales a dicho ámbito, construidos exitosamente
Una excepción causa que el flujo del programa salga de todos los bloques, entre el throw y el correspondiente catch
Garantías de seguridad
Ninguna pieza de código individual requiere el mismo grado de tolerancia a fallas
Un buen manejo de errores es multinivel
La seguridad frente a excepciones implica un examen cuidadoso de operaciones individuales
El estándar provee un conjunto (razonable) de niveles de garantías de seguridad frente a las excepciones
La librería estándar puede ser utilizable en esencialmente cualquier programa
Por tanto, proporciona un buen ejemplo
Los conceptos, técnicas y enfoques de diseño son más importantes que los detalles
Garantías de seguridad de las
excepciones
Sin garantías
Si una función lanza una excepción, puede suceder cualquier
cosa
Los datos pueden corromperse o no ser válidos luego de una excepción Use RAII, aún cuando no utilice excepciones
Garantías de seguridad de las
excepciones
Garantía básica (no pérdida)
Si se lanza una excepción, las invariantes de un objeto son
aún válidas (no se corrompen estructuras de datos ni objetos, no hay “punteros colgados”) y, no se pierden recursos (por ej. toda la memoria asignada es retornada apropiadamente), las operaciones pueden quedar a medias
(aunque los objetos quedan en estado consistente)
Se debería soportar, por lo menos, esta garantía para todas
Garantía básica
Los recursos usados deben ser destructibles
(posiblemente cuando se “desenrolla” la pila, o sea se supone que el destructor no lanzará excepciones) y
usables aún luego de una excepción
No hay garantías que el contenido de los datos sean los mismos que antes de invocar la función donde se generó la excepción (estado válido aunque no previsible)
Si el componente tiene muchos estados válidos, luego de una
excepción no tenemos idea en que estado estará el componente, sólo que es válido.
Las opciones de recuperación en este caso son limitadas: destruir
o, resetear el componente a un estado conocido antes de un uso posterior
Garantía básica
Puede ser la mejor elección cuando una garantía
firme (strong) es demasiado costosa en términos de
memoria o de performance
Frecuentemente este nivel es suficiente para el manejo de errores
Apropiadas cuando el invocante puede manejar operaciones fallidas que cambiaron el estado de objetos
Asegúrese que su destructor no lanzará excepciones bajo ninguna circunstancia
Tenga especial cuidado para mantener cualquier invariante que haya definido
Excepciones desde un
destructor
Un destructor puede invocarse:
Invocación normal: un objeto sale de su ámbito, delete, etc.
Invocación durante manejo de excepción: Durante el “desarmado de la pila” por el mecanismo de manejo de excepciones, se sale del ámbito de un objeto que posee un destructor
Excepciones desde un destructor
Las excepciones nunca deberían dejar el destructor. En caso de lanzarse una excepción en el destructor
cuando estaba siendo invocado al producirse otra
excepción y se estaba “desenrollando la pila” (no pueden propagarse dos excepciones simultáneamente), en este caso se invoca a std::terminate() y la aplicación muere!
Se pueden lanzar excepciones dentro del destructor pero,
nunca deberían escapar del mismo.
La librería estándar supone que, ni los destructores, ni las funciones de desasignación (ej.:delete), ni swap (por
Excepciones desde un
destructor
Excepciones desde un
destructor
Excepciones desde un
destructor
Los contenedores de la librería estándar no permiten construir contenedores de objetos cuyo destructor pueda lanzar excepciones
Si el destructor invoca a funciones que pueden lanzar excepciones, debe protegerse a sí mismo. Esto
permite especificar diferentes acciones dentro del destructor dependiendo si, un objeto es destruido normalmente o, como parte del desapilado
Garantía firme (
Commit or
rollback
)
Si se lanza una excepción, el estado de los objetos deben quedar como antes de que esto ocurra (rollback). Es
decir, si una operación falla debe garantizarse que no tendrá efectos secundarios
Una operación debe ser exitosa (commit) o rolled back (a veces demasiado costoso), nunca parcialmente completa
std::vector::push_back(a)
insert 1 solo elemento en una list uninitialized_copy()
Garantía firme
Las llamadas a las funciones con esta garantía son
atómicas en el sentido de que, si lo hacen con éxito lo
hacen completamente y, si fallan, es como si el programa nunca las hubiese llamado
Esta opción a veces penaliza la performance. Debería implementarse cuando es “natural” y libre para
hacerlo y, sólo en operaciones claves.
En la práctica, no se requiere tan a menudo como se esperaría puesto que, la garantía básica asegura que no se perderán recursos ni se violarán invariantes, sabiendo que el programa aún estará en un estado válido (aunque no el ideal)
Garantía firme (copy y swap)
Piense en una línea crítica que divide lo que está arriba (prepare: puede lanzar excepciones pero no modifica los datos originales), de lo que está debajo (commit: no debe lanzar excepciones y modifica los objetos originales, std::swap() puede ayudar en esta parte)
Puede ser muy difícil escribir código con esta garantía; sino se puede dividir en 2 fases, sea cuidadoso!
Garantía firme
Las operaciones que pueden lanzar excepciones se realizan
sobre una copia de los datos; si se realizan exitosamente, el objeto actual y su copia (ahora exitosamente
Garantía firme
temp es destruida al retornar (por destructor) y si ocurre una excepción, se destruye al desarmar la pila
Lectura complementaria
Generic: Change the Way You Write Exception-Safe Code — Forever
(https://www.drdobbs.com/article/print?articleId=18440
3758&siteSectionName=cpp)
Usan técnicas de programación genérica para ayudar a escribir código con garantía firme de seguridad frente a excepciones.
Escribir código correcto en presencia de excepciones no es una
tarea fácil
ScopeGuard clase genérica para un rollback, que puede
cancelarse si la acción intentada es exitosa (commited)
Crea una clase ScopeGuard que si se necesita ejecutar
operaciones “todo o nada”, se añade una línea de código donde se instancia un objeto de esta clase después de cada una de estas operaciones
Lectura complementaria
ScopeGuard permite escribir código transaccional que permite deshacer el código que precede la creación de un objeto ScopeGuard si el siguiente código lanza una excepción
Luego de la versión C++11 muchos reimplementaron, a partir de este artículo, esta facilidad extremadamente importante para seguridad frente a excepciones.
La librería Boost tiene la suya:
https://www.boost.org/doc/libs/1_54_0/libs/scope_exit/ doc/html/index.html
Garantía nothrow (Failure
transparency)
Bajo ninguna circunstancia, la función no genera ni permite propagar ninguna excepción, ni usa funciones que lo
hagan.
Atrapan y manejan todas las excepciones lanzadas por las operaciones que usan. Siempre terminan exitosamente
Marcarlas como noexcept
Los destructores deberían tener este nivel de garantía, ídem con las funciones para desasignar y clean-up
(funciones clear/erase/reset en contenedores)
Esta garantía es provista por unas pocas operaciones
simples de la librería estándar tales como swap() y
Funciones C como fclose() no pueden lanzar excepciones (a menos que tomen argumentos funciones que sí lo hacen, tal como qsort() o
bsearch() que toman como argumentos punteros a funciones como argumentos)
Todas las funciones que trabajan sólo con tipos de datos primitivos son nothrow.
Ojo! Operaciones inocentes como <, =, ==, != o sort() pueden lanzar excepciones
La seguridad con respecto a excepciones se obtiene si algunas funciones (críticas) sostienen esta garantía
Garantía no throw
Es el nivel más alto de seguridad, aunque no siempre el mejor
Las excepciones no son malas, son una herramienta para
tratar con situaciones problemáticas y, esta garantía la prohibe
Working Draft, Standard for Programming Language C++ N3337
Reglas generales
Decida qué nivel de tolerancia a fallas necesita
No todos los fragmentos de código necesitan ser seguros
frente a las excepciones
Apunte a proveer una garantía firme y (siempre),
provea una garantía básica sino puede permitirse una garantía firme
Mantenga un buen estado (generalmente el anterior) hasta
que haya construido uno nuevo, luego realice la actualización “atómicamente”
Defina “buen estado” (invariante) cuidadosamente
Establezca las invariantes en el constructor (no en funciones
Reglas generales
Minimice los bloques try explícitos
Represente los recursos directamente
Prefiera RAII en el código donde sea posible
Evite manejo de memoria ad hoc con new/delete. Piense en
funciones que asignan recursos y retornan punteros “básicos” (fopen, malloc, strdup,etc.)
Mantenga el código altamente estructurado (“estilizado”)
El “código aleatorio” oculta ciertamente problemas con las