El manejo de excepciones (EH) parece ser el estándar actual, y al buscar en la web, no puedo encontrar ideas o métodos novedosos que intenten mejorarlo o reemplazarlo (bueno, existen algunas variaciones, pero nada novedoso).
Aunque la mayoría de la gente parece ignorarlo o simplemente aceptarlo, EH tiene algunos inconvenientes enormes: las excepciones son invisibles para el código y crea muchos, muchos puntos de salida posibles. Joel sobre software escribió un artículo al respecto . La comparación con los goto
ajustes perfectos me hizo pensar nuevamente en EH.
Intento evitar EH y solo uso valores de retorno, devoluciones de llamada o lo que sea apropiado para el propósito. Pero cuando tiene que escribir código confiable, simplemente no puede ignorar EH en estos días : comienza con el new
, que puede generar una excepción, en lugar de simplemente devolver 0 (como en los viejos tiempos). Esto hace que cualquier línea de código C ++ sea vulnerable a una excepción. Y luego más lugares en el código fundacional de C ++ lanzan excepciones ... std lib lo hace, y así sucesivamente.
Esto se siente como caminar sobre terrenos inestables . Entonces, ¡ahora nos vemos obligados a tener cuidado con las excepciones!
Pero es difícil, es realmente difícil. Debe aprender a escribir código seguro de excepción, e incluso si tiene algo de experiencia con él, ¡aún será necesario que verifique nuevamente cualquier línea de código para estar seguro! O comienzas a poner bloques try / catch en todas partes, lo que satura el código hasta que alcanza un estado de ilegibilidad.
EH reemplazó el antiguo enfoque determinista limpio (valores de retorno ...), que tenía solo algunos inconvenientes, pero comprensibles y fácilmente solucionables, con un enfoque que crea muchos puntos de salida posibles en su código, y si comienza a escribir código que atrapa excepciones (qué se ven obligados a hacerlo en algún momento), incluso crea una multitud de rutas a través de su código (código en los bloques catch, piense en un programa de servidor donde necesita instalaciones de registro que no sean std :: cerr ..). EH tiene ventajas, pero ese no es el punto.
Mis preguntas actuales:
- ¿Realmente escribes código seguro de excepción?
- ¿Está seguro de que su último código "listo para producción" es seguro para excepciones?
- ¿Puedes estar seguro de que lo es?
- ¿Conoces y / o utilizas alternativas que funcionan?
fuente
Respuestas:
Su pregunta hace una afirmación, que "Escribir código seguro de excepción es muy difícil". Primero responderé sus preguntas y luego responderé la pregunta oculta detrás de ellas.
Respondiendo preguntas
Por supuesto que sí.
Esta es la razón por la que Java perdió mucho de su atractivo para mí como programador de C ++ (falta de semántica RAII), pero estoy divagando: esta es una pregunta de C ++.
De hecho, es necesario cuando necesita trabajar con código STL o Boost. Por ejemplo, los subprocesos de C ++ (
boost::thread
ostd::thread
) generarán una excepción para salir con gracia.Escribir código seguro de excepción es como escribir código libre de errores.
No puede estar 100% seguro de que su código sea seguro para excepciones. Pero luego, se esfuerza por lograrlo, utilizando patrones conocidos y evitando los antipatrones conocidos.
No hay alternativas viables en C ++ (es decir, deberá volver a C y evitar las bibliotecas de C ++, así como sorpresas externas como Windows SEH).
Escribir código seguro de excepción
Para escribir un código de seguridad de excepción, primero debe saber qué nivel de seguridad de excepción tiene cada instrucción que escriba.
Por ejemplo, a
new
puede lanzar una excepción, pero la asignación de un incorporado (por ejemplo, un int o un puntero) no fallará. Un intercambio nunca fallará (nunca escriba un intercambio de lanzamiento), unstd::list::push_back
lanzamiento de latas ...Garantía de excepción
Lo primero que debe entender es que debe poder evaluar la garantía de excepción que ofrecen todas sus funciones:
Ejemplo de código
El siguiente código parece correcto C ++, pero en verdad, ofrece la garantía "ninguno" y, por lo tanto, no es correcto:
Escribo todo mi código con este tipo de análisis en mente.
La garantía más baja que se ofrece es básica, pero luego, el orden de cada instrucción hace que toda la función sea "ninguna", porque si 3. arroja, x se perderá.
Lo primero que debe hacer es hacer que la función sea "básica", es decir, poner x en un puntero inteligente hasta que sea propiedad de la lista de forma segura:
Ahora, nuestro código ofrece una garantía "básica". Nada se escapará y todos los objetos estarán en un estado correcto. Pero podríamos ofrecer más, es decir, la garantía sólida. Aquí es donde puede llegar a ser costoso, y es por eso que no todo el código C ++ es fuerte. Vamos a intentarlo:
Reordenamos las operaciones, primero creando y configurando
X
su valor correcto. Si alguna operación falla, entoncest
no se modifica, por lo tanto, la operación 1 a 3 puede considerarse "fuerte": si algo arroja,t
no se modifica yX
no se filtrará porque es propiedad del puntero inteligente.Luego, creamos una copia
t2
det
, y trabajamos en esta copia de la operación 4 a 7. Si algo arroja,t2
se modifica, pero entonces,t
sigue siendo el original. Todavía ofrecemos la garantía fuerte.Luego, intercambiamos
t
yt2
. Las operaciones de intercambio no deben arrojarse en C ++, por lo tanto, esperemos que el intercambio para el que escribió noT
sea un lanzamiento (si no es así, vuelva a escribirlo para que no sea un lanzamiento).Entonces, si llegamos al final de la función, todo tiene éxito (No es necesario un tipo de retorno) y
t
tiene su valor exceptuado. Si falla,t
aún tiene su valor original.Ahora, ofrecer la garantía sólida podría ser bastante costoso, así que no se esfuerce por ofrecer la garantía sólida a todo su código, pero si puede hacerlo sin costo (y la incorporación de C ++ y otras optimizaciones podrían hacer que todo el código anterior sea gratuito) , entonces hacerlo. El usuario de la función te lo agradecerá.
Conclusión
Se necesita algún hábito para escribir código seguro de excepción. Deberá evaluar la garantía ofrecida por cada instrucción que utilizará y luego deberá evaluar la garantía ofrecida por una lista de instrucciones.
Por supuesto, el compilador de C ++ no hará una copia de seguridad de la garantía (en mi código, ofrezco la garantía como una etiqueta @warning doxygen), lo cual es un poco triste, pero no debería impedir que intente escribir código seguro de excepción.
Falla normal vs. error
¿Cómo puede un programador garantizar que una función sin fallas siempre tenga éxito? Después de todo, la función podría tener un error.
Esto es verdad. Se supone que las garantías de excepción se ofrecen mediante un código libre de errores. Pero luego, en cualquier idioma, llamar a una función supone que la función está libre de errores. Ningún código cuerdo se protege contra la posibilidad de que tenga un error. Escriba el código lo mejor que pueda, y luego, ofrezca la garantía con la suposición de que está libre de errores. Y si hay un error, corríjalo.
Las excepciones son para fallas de procesamiento excepcionales, no para errores de código.
Ultimas palabras
Ahora, la pregunta es "¿Vale la pena?".
Por supuesto que es. Tener una función "nothrow / no-fail" sabiendo que la función no fallará es una gran bendición. Lo mismo puede decirse de una función "fuerte", que le permite escribir código con semántica transaccional, como bases de datos, con funciones de confirmación / reversión, la confirmación es la ejecución normal del código, arrojando excepciones siendo la reversión.
Entonces, lo "básico" es la mínima garantía que debe ofrecer. C ++ es un lenguaje muy fuerte allí, con sus ámbitos, lo que le permite evitar cualquier pérdida de recursos (algo que un recolector de basura encontraría difícil de ofrecer para la base de datos, la conexión o los identificadores de archivo).
Así, por lo que yo veo, es digno de él.
Editar 2010-01-29: Acerca del intercambio sin lanzamiento
nobar hizo un comentario que creo que es bastante relevante, porque es parte de "cómo se escribe el código seguro de excepción":
swap()
funciones escritas a medida . Cabe señalar, sin embargo, questd::swap()
puede fallar en función de las operaciones que utiliza internamenteel valor predeterminado
std::swap
hará copias y asignaciones que, para algunos objetos, pueden arrojar. Por lo tanto, el intercambio predeterminado podría arrojarse, ya sea utilizado para sus clases o incluso para clases STL. En lo que respecta al estándar C ++, la operación de intercambio paravector
,deque
ylist
no arrojará, mientras que podría hacerlomap
si el functor de comparación puede lanzar en la construcción de la copia (Ver El lenguaje de programación C ++, Edición especial, apéndice E, E.4.3 .Swap ).Al observar la implementación de Visual C ++ 2008 del intercambio del vector, el intercambio del vector no se lanzará si los dos vectores tienen el mismo asignador (es decir, el caso normal), pero hará copias si tienen asignadores diferentes. Y así, supongo que podría arrojar en este último caso.
Por lo tanto, el texto original aún se mantiene: nunca escriba un intercambio de lanzamiento, pero debe recordarse el comentario de nobar: asegúrese de que los objetos que está intercambiando tengan un intercambio de no lanzamiento.
Editar 2011-11-06: artículo interesante
Dave Abrahams , quien nos brindó las garantías básicas / fuertes / no críticas , describió en un artículo su experiencia sobre cómo hacer que la excepción STL sea segura:
http://www.boost.org/community/exception_safety.html
Mire el séptimo punto (Pruebas automatizadas para seguridad de excepción), donde confía en las pruebas unitarias automatizadas para asegurarse de que se analicen todos los casos. Supongo que esta parte es una excelente respuesta a la pregunta del autor " ¿Puedes estar seguro de que lo es? ".
Editar 2013-05-31: Comentario de dionadar
Dionadar se refiere a la siguiente línea, que de hecho tiene un comportamiento indefinido.
La solución aquí es verificar si el número entero ya está en su valor máximo (usando
std::numeric_limits<T>::max()
) antes de hacer la suma.Mi error iría en la sección "Error normal vs. error", es decir, un error. No invalida el razonamiento, y no significa que el código seguro de excepción sea inútil porque es imposible de lograr. No puede protegerse contra el apagado de la computadora, o los errores del compilador, o incluso sus errores u otros errores. No puedes alcanzar la perfección, pero puedes intentar acercarte lo más posible.
Corregí el código con el comentario de Dionadar en mente.
fuente
"finally" does exactly what you were trying to achieve
Por supuesto que sí. Y debido a que es fácil producir código frágilfinally
, la noción "probar con recursos" finalmente se introdujo en Java 7 (es decir, 10 años después de C #using
y 3 décadas después de los destructores de C ++). Esta es la fragilidad que critico. En cuanto aJust because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing"
: en esto, la industria no está de acuerdo con su gusto, ya que los lenguajes recolectados de basura tienden a agregar declaraciones inspiradas en RAII (C #using
y Javatry
).RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimes
No, no lo hace. Puede utilizar punteros inteligentes o clases de utilidad para "proteger" los recursos.Escribir código seguro para excepciones en C ++ no se trata tanto de usar muchos bloques try {} catch {}. Se trata de documentar qué tipo de garantías proporciona su código.
Recomiendo leer la serie Guru Of The Week de Herb Sutter , en particular las entregas 59, 60 y 61.
Para resumir, hay tres niveles de seguridad de excepción que puede proporcionar:
Personalmente, descubrí estos artículos bastante tarde, por lo que gran parte de mi código C ++ definitivamente no es seguro para excepciones.
fuente
Algunos de nosotros hemos estado usando la excepción por más de 20 años. PL / I los tiene, por ejemplo. La premisa de que son una tecnología nueva y peligrosa me parece cuestionable.
fuente
En primer lugar (como dijo Neil), SEH es el Manejo de excepciones estructuradas de Microsoft. Es similar pero no idéntico al procesamiento de excepciones en C ++. De hecho, debe habilitar el Control de excepciones de C ++ si lo desea en Visual Studio: ¡el comportamiento predeterminado no garantiza que los objetos locales se destruyan en todos los casos! En cualquier caso, el manejo de excepciones no es realmente más difícil , solo es diferente .
Ahora para sus preguntas reales.
Si. Me esfuerzo por un código de excepción seguro en todos los casos. Evangelizo usando técnicas RAII para el acceso al alcance de los recursos (p. Ej.,
boost::shared_ptr
Para memoria,boost::lock_guard
para bloqueo). En general, el uso constante de RAII y las técnicas de protección de alcance harán que el código seguro de excepción sea mucho más fácil de escribir. El truco es aprender qué existe y cómo aplicarlo.No. Es tan seguro como es. Puedo decir que no he visto una falla en el proceso debido a una excepción en varios años de actividad 24/7. No espero un código perfecto, solo un código bien escrito. Además de proporcionar una seguridad excepcional, las técnicas anteriores garantizan la corrección de una manera que es casi imposible de lograr con
try
/catch
blocks. Si está atrapando todo en su ámbito de control superior (hilo, proceso, etc.), puede estar seguro de que continuará ejecutándose ante las excepciones (la mayoría de las veces ). Las mismas técnicas también lo ayudarán a continuar ejecutándose correctamente ante excepciones sintry
/catch
bloques en todas partes .Si. Puede estar seguro mediante una exhaustiva auditoría de código, pero ¿nadie realmente hace eso? Sin embargo, las revisiones periódicas de código y los desarrolladores cuidadosos hacen mucho para llegar allí.
He intentado algunas variaciones a lo largo de los años, como los estados de codificación en los bits superiores (ala
HRESULT
s ) o ese horriblesetjmp() ... longjmp()
truco. Ambos se descomponen en la práctica, aunque de maneras completamente diferentes.Al final, si te acostumbras a aplicar algunas técnicas y piensas cuidadosamente dónde puedes hacer algo en respuesta a una excepción, terminarás con un código muy legible que es seguro para las excepciones. Puede resumir esto siguiendo estas reglas:
try
/catch
cuándo puede hacer algo sobre una excepción específicanew
odelete
en códigostd::sprintf
,snprintf
y matrices en general: se usastd::ostringstream
para formatear y reemplazar matrices constd::vector
ystd::string
Solo puedo recomendarle que aprenda a usar las excepciones correctamente y que se olvide de los códigos de resultados si planea escribir en C ++. Si desea evitar excepciones, puede considerar escribir en otro idioma que no las tenga o las haga seguras . Si realmente quiere aprender a utilizar C ++ por completo, lea algunos libros de Herb Sutter , Nicolai Josuttis y Scott Meyers .
fuente
new
odelete
en código": por raw supongo que te refieres fuera de un constructor o destructor.delete
nunca debe usarse fuera de la implementación detr1::shared_ptr
y similares.new
se puede usar siempre que su uso sea similartr1::shared_ptr<X> ptr(new X(arg, arg));
. La parte importante es que el resultado denew
va directamente a un puntero administrado. La página deboost::shared_ptr
Mejores prácticas describe lo mejor.No es posible escribir código seguro de excepción bajo el supuesto de que "cualquier línea puede lanzar". El diseño del código seguro para excepciones se basa de manera crítica en ciertos contratos / garantías que se supone que debe esperar, observar, seguir e implementar en su código. Es absolutamente necesario tener un código que garantice que nunca se lanzará. Existen otros tipos de garantías de excepción por ahí.
En otras palabras, la creación de código seguro para excepciones es en gran medida una cuestión de diseño del programa , no solo una cuestión de codificación simple .
fuente
Bueno, ciertamente tengo la intención de hacerlo.
Estoy seguro de que mis servidores 24/7 creados con excepciones se ejecutan 24/7 y no pierden memoria.
Es muy difícil asegurarse de que cualquier código sea correcto. Por lo general, uno solo puede ir por resultados
No. Usar excepciones es más limpio y fácil que cualquiera de las alternativas que he usado durante los últimos 30 años en la programación.
fuente
Dejando de lado la confusión entre las excepciones SEH y C ++, debe tener en cuenta que las excepciones se pueden lanzar en cualquier momento y escribir su código con eso en mente. La necesidad de seguridad de excepción es en gran medida lo que impulsa el uso de RAII, punteros inteligentes y otras técnicas modernas de C ++.
Si sigue los patrones bien establecidos, escribir código seguro de excepción no es particularmente difícil, y de hecho es más fácil que escribir código que maneje los retornos de error correctamente en todos los casos.
fuente
EH es bueno, en general. Pero la implementación de C ++ no es muy amigable, ya que es muy difícil saber qué tan buena es su cobertura de captura de excepciones. Java, por ejemplo, lo hace fácil, el compilador tenderá a fallar si no maneja posibles excepciones.
fuente
noexcept
.Aunque realmente me gusta trabajar con Eclipse y Java (nuevo en Java), porque arroja errores en el editor si le falta un controlador EH. Eso hace que las cosas sean MUCHO más difíciles de olvidar para manejar una excepción ...
Además, con las herramientas IDE, agrega el bloque try / catch u otro bloque catch automáticamente.
fuente
Algunos de nosotros preferimos lenguajes como Java que nos obligan a declarar todas las excepciones lanzadas por los métodos, en lugar de hacerlos invisibles como en C ++ y C #.
Cuando se hace correctamente, las excepciones son superiores a los códigos de retorno de error, si no es por otra razón que no tiene que propagar fallas manualmente en la cadena de llamadas.
Dicho esto, la programación de la biblioteca API de bajo nivel probablemente debería evitar el manejo de excepciones y atenerse a los códigos de retorno de error.
Según mi experiencia, es difícil escribir un código de manejo de excepciones limpio en C ++. Termino usando
new(nothrow)
mucho.fuente
new(std::nothrow)
no es suficiente. Por cierto, es más fácil escribir código seguro de excepciones en C ++ que en Java: en.wikipedia.org/wiki/Resource_Acquisition_Is_InitializationHago mi mejor esfuerzo para escribir código seguro de excepción, sí.
Eso significa que me cuido de vigilar qué líneas pueden lanzar. No todos pueden hacerlo, y es muy importante tenerlo en cuenta. La clave es realmente pensar y diseñar su código para satisfacer las garantías de excepción definidas en el estándar.
¿Se puede escribir esta operación para proporcionar la garantía de excepción fuerte? ¿Tengo que conformarme con el básico? ¿Qué líneas pueden arrojar excepciones y cómo puedo asegurarme de que si lo hacen, no corrompan el objeto?
fuente
¿Realmente escribes código seguro de excepción? [No hay tal cosa. Las excepciones son un escudo de papel a los errores a menos que tenga un entorno administrado. Esto se aplica a las primeras tres preguntas.]
¿Conoces y / o utilizas alternativas que funcionan? [¿Alternativa a qué? El problema aquí es que las personas no separan los errores reales del funcionamiento normal del programa. Si es la operación normal del programa (es decir, no se encuentra un archivo), en realidad no se trata de manejo de errores. Si se trata de un error real, no hay forma de "manejarlo" o no es un error real. Su objetivo aquí es averiguar qué salió mal y detener la hoja de cálculo y registrar un error, reiniciar el controlador de su tostadora, o simplemente rezar para que el jetfighter pueda continuar volando incluso cuando su software tiene errores y esperar lo mejor.]
fuente
Mucha gente (incluso diría que la mayoría) lo hace.
Lo que es realmente importante acerca de las excepciones es que si no escribe ningún código de manejo, el resultado es perfectamente seguro y se comporta bien. Demasiado ansioso por entrar en pánico, pero seguro.
Usted necesita activamente cometer errores en los controladores para conseguir algo insegura, y sólo captura (...) {} comparará a ignorar el código de error.
fuente
f = new foo(); f->doSomething(); delete f;
si el método doSomething produce una excepción, entonces tiene una pérdida de memoria.