¿(Realmente) escribes código seguro de excepción? [cerrado]
El manejo de excepciones (EH) parece ser el estándar actual y, al buscar en la web, no puedo encontrar ninguna idea o método novedoso que intente 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 congoto
ajuste perfecto me hizo pensar nuevamente en EH.
Intento evitar EH y solo uso valores de retorno, devoluciones de llamada o lo que sea que se ajuste al propósito. Pero cuando tienes que escribir código confiable, no puedes ignorar EH hoy en día : comienza con new
, lo 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 fundamental de C++ arrojan excepciones... std lib lo hace, y así sucesivamente.
Esto se siente como caminar sobre terreno inestable. ... ¡Así que ahora nos vemos obligados a preocuparnos por las excepciones!
Pero es difícil, es muy difícil. Tienes que aprender a escribir código seguro de excepción, e incluso si tienes algo de experiencia con él, aún será necesario verificar cada línea de código para estar seguro. O empiezas a poner bloques try/catch por 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 detecte excepciones (lo que se ven obligados a hacerlo en algún momento), luego 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 funciones de registro distintas a std::cerr ..). EH tiene ventajas, pero ese no es el punto.
Mis preguntas reales:
- ¿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 siquiera estar seguro de que lo es?
- ¿Conoce y/o utiliza realmente alternativas que funcionen?
Su pregunta afirma que "Escribir código seguro para excepciones es muy difícil". Primero responderé a tus preguntas y luego responderé a la pregunta oculta detrás de ellas.
Respondiendo preguntas
¿Realmente escribes código seguro de excepción?
Por supuesto que sí.
Esta es la razón por la que Java perdió gran parte 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 necesitas trabajar con código STL o Boost. Por ejemplo, los subprocesos de C++ ( boost::thread
o std::thread
) generarán una excepción para salir correctamente.
¿Está seguro de que su último código "listo para producción" es seguro para excepciones?
¿Puedes siquiera estar seguro de que lo es?
Escribir código seguro para excepciones es como escribir código sin errores.
No puede estar 100% seguro de que su código sea seguro para excepciones. Pero luego, te esfuerzas por lograrlo, utilizando patrones conocidos y evitando antipatrones conocidos.
¿Conoce y/o utiliza realmente alternativas que funcionen?
No existen 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 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 generar una excepción, pero la asignación de un elemento integrado (por ejemplo, un int o un puntero) no fallará. Un swap nunca fallará (nunca escribas un swap de lanzamiento), un std::list::push_back
can throw...
Garantía de excepción
Lo primero que debes entender es que debes poder evaluar la garantía de excepción que ofrecen todas tus funciones:
- none : Su código nunca debería ofrecer eso. Este código filtrará todo y se descompondrá en la primera excepción lanzada.
- básico : esta es la garantía que debes ofrecer como mínimo, es decir, si se produce una excepción, no se filtran recursos y todos los objetos siguen completos.
- fuerte : el procesamiento tendrá éxito o generará una excepción, pero si se produce, los datos estarán en el mismo estado que si el procesamiento no hubiera comenzado en absoluto (esto le da poder transaccional a C++)
- nothrow/nofail : el procesamiento se realizará correctamente.
Ejemplo de código
El siguiente código parece C++ correcto, pero en realidad ofrece la garantía "ninguno" y, por lo tanto, no es correcto:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Escribo todo mi código con este tipo de análisis en mente.
La garantía más baja ofrecida 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 filtrará.
Lo primero que debe hacer sería hacer que la función sea "básica", es decir, poner x en un puntero inteligente hasta que sea propiedad segura de la lista:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Ahora, nuestro código ofrece una garantía "básica". No se filtrará nada y todos los objetos estarán en el estado correcto. Pero podríamos ofrecer más, es decir, una garantía sólida. Aquí es donde puede resultar costoso y es por eso que no todo el código C++ es sólido. Vamos a intentarlo:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Reordenamos las operaciones, primero creando y estableciendo X
su valor correcto. Si alguna operación falla, t
no se modifica, por lo que las operaciones 1 a 3 pueden considerarse "fuertes": si algo se produce, t
no se modifica y X
no se filtrará porque pertenece al puntero inteligente.
Luego, creamos una copia t2
de t
y trabajamos en esta copia desde la operación 4 a la 7. Si algo falla, t2
se modifica, pero t
sigue siendo el original. Todavía ofrecemos la fuerte garantía.
Luego, intercambiamos t
y t2
. Las operaciones de intercambio no deben realizarse en C++, así que esperemos que el intercambio que usted escribió T
no lo sea (si no lo es, reescríbalo para que no lo sea).
Entonces, si llegamos al final de la función, todo tuvo éxito (no es necesario un tipo de retorno) y t
tiene su valor exceptuado. Si falla, t
todavía conserva su valor original.
Ahora bien, ofrecer una garantía sólida podría ser bastante costoso, así que no se esfuerce por ofrecer una 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 no tenga costo) , entonces hacerlo. El usuario de la función te lo agradecerá.
Conclusión
Se necesita cierta costumbre para escribir código seguro para excepciones. Deberá evaluar la garantía que ofrece cada instrucción que utilizará y luego deberá evaluar la garantía que ofrece una lista de instrucciones.
Por supuesto, el compilador de C++ no respaldará 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 impedirle intentar escribir código seguro para excepciones.
Fallo normal versus error
¿Cómo puede un programador garantizar que una función sin fallas siempre tendrá éxito? Después de todo, la función podría tener un error.
Esto es cierto. Se supone que las garantías de excepción se ofrecen mediante un código libre de errores. Pero entonces, en cualquier idioma, llamar a una función supone que la función está libre de errores. Ningún código sensato se protege a sí mismo contra la posibilidad de que tenga un error. Escriba el código lo mejor que pueda y luego ofrezca la garantía suponiendo que esté libre de errores. Y si hay algún error, corríjalo.
Las excepciones son por fallas de procesamiento excepcionales, no por errores de código.
Ultimas palabras
Ahora la pregunta es "¿Vale la pena?".
Por supuesto que es. Tener una función "sin lanzamiento/sin falla" sabiendo que la función no fallará es una gran ayuda. 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, siendo la confirmación la ejecución normal del código y las excepciones que arrojan la reversión.
Entonces, lo "básico" es la mínima garantía que debes ofrecer. C++ es un lenguaje muy potente allí, con sus alcances que le permiten evitar fugas de recursos (algo que a un recolector de basura le resultaría difícil ofrecer para la base de datos, la conexión o los identificadores de archivos).
Así que, a mi modo de ver, merece la pena.
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 código seguro de excepción":
- [yo] Un intercambio nunca fallará (ni siquiera escribas un intercambio de lanzamiento)
- [nobar] Esta es una buena recomendación para
swap()
funciones escritas a medida. Cabe señalar, sin embargo, questd::swap()
puede fallar en función de las operaciones que utiliza internamente.
De forma predeterminada, std::swap
se realizarán copias y asignaciones que, para algunos objetos, pueden arrojarse. Por lo tanto, el intercambio predeterminado podría generarse, ya sea utilizado para sus clases o incluso para clases STL. En lo que respecta al estándar C++, la operación de intercambio para vector
, deque
y list
no arrojará, mientras que podría hacerlo map
si el functor de comparación puede arrojar en la construcción de copia (consulte El lenguaje de programación C++, edición especial, apéndice E, E.4.3 .Intercambio ).
Al observar la implementación del intercambio de vectores en Visual C++ 2008, el intercambio de vectores no se generará 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 fallar en este último caso.
Entonces, el texto original aún se mantiene: nunca escribas un intercambio de lanzamiento, pero debes recordar el comentario de nobar: asegúrate de que los objetos que estás intercambiando tengan un intercambio de no lanzamiento.
Editar 2011-11-06: Artículo interesante
Dave Abrahams , quien nos brindó las garantías básica/fuerte/sin lanzamiento , 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 excepciones), donde se basa en pruebas unitarias automatizadas para asegurarse de que se prueben todos los casos. Supongo que esta parte es una excelente respuesta a la pregunta del autor "¿ Puedes estar seguro de que así es? ".
Editar 2013-05-31: Comentario de dionadar
t.integer += 1;
no tiene la garantía de que no se producirá un desbordamiento, NO es una excepción segura y, de hecho, técnicamente puede invocar UB. (El desbordamiento firmado es UB: C++11 5/4 "Si durante la evaluación de una expresión, el resultado no está definido matemáticamente o no está en el rango de valores representables para su tipo, el comportamiento no está definido".) Tenga en cuenta que unsigned Los números enteros no se desbordan, pero realizan sus cálculos en una clase de equivalencia módulo 2^#bits.
Dionadar se refiere a la siguiente línea, que de hecho tiene un comportamiento indefinido.
t.integer += 1 ; // 1. nothrow/nofail
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 realizar la suma.
Mi error iría en la sección "Fallo normal versus error", es decir, un error. No invalida el razonamiento y no significa que el código seguro para excepciones sea inútil porque sea imposible de lograr. No puede protegerse contra el apagado de la computadora, o contra errores del compilador, o incluso contra sus propios errores, u otros errores. No puedes alcanzar la perfección, pero puedes intentar acercarte lo más posible.
Corregí el código teniendo en cuenta el comentario de Dionadar.
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 ofrece su código.
Recomiendo leer la serie Guru Of The Week de Herb Sutter , en particular las entregas 59, 60 y 61.
En resumen, hay tres niveles de seguridad excepcional que puede proporcionar:
- Básico: cuando su código genera una excepción, su código no pierde recursos y los objetos siguen siendo destructibles.
- Fuerte: cuando su código genera una excepción, deja el estado de la aplicación sin cambios.
- Sin lanzamiento: su código nunca arroja excepciones.
Personalmente, descubrí estos artículos bastante tarde, por lo que gran parte de mi código C++ definitivamente no es seguro para excepciones.
Algunos de nosotros hemos estado usando la excepción durante 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.
En primer lugar (como dijo Neil), SEH es el manejo estructurado de excepciones de Microsoft. Es similar, pero no idéntico, al procesamiento de excepciones en C++. De hecho, debe habilitar el manejo 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 , simplemente es diferente .
Ahora para sus preguntas reales.
¿Realmente escribes código seguro de excepción?
Sí. Me esfuerzo por lograr un código seguro de excepción en todos los casos. Evangelizo usando técnicas RAII para el acceso específico a recursos (por ejemplo, boost::shared_ptr
para memoria, boost::lock_guard
para bloqueo). En general, el uso constante de RAII y técnicas de protección del alcance hará que el código seguro de excepción sea mucho más fácil de escribir. El truco consiste en aprender qué existe y cómo aplicarlo.
¿Está seguro de que su último código "listo para producción" es seguro para excepciones?
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 horas al día, 7 días a la semana. No espero un código perfecto, sólo un código bien escrito. Además de brindar una seguridad excepcional, las técnicas anteriores garantizan la corrección de una manera que es casi imposible de lograr con try
/ catch
bloques. Si captura todo lo que está en su alcance de control superior (subproceso, proceso, etc.), puede estar seguro de que continuará ejecutándose frente a excepciones ( la mayor parte del tiempo ). Las mismas técnicas también le ayudarán a seguir ejecutando correctamente frente a excepciones sin bloques try
/catch
en todas partes .
¿Puedes siquiera estar seguro de que lo es?
Sí. Puede estar seguro mediante una auditoría exhaustiva del código, pero nadie lo hace realmente. Sin embargo, las revisiones periódicas del código y los desarrolladores cuidadosos contribuyen en gran medida a lograrlo.
¿Conoce y/o utiliza realmente alternativas que funcionen?
He probado algunas variaciones a lo largo de los años, como codificar estados en los bits superiores (ala HRESULT
s ) o ese horrible setjmp() ... longjmp()
truco. Ambos fracasan en la práctica, aunque de maneras completamente diferentes.
Al final, si adquiere el hábito de aplicar algunas técnicas y pensar detenidamente dónde puede hacer algo en respuesta a una excepción, terminará con un código muy legible que es seguro para excepciones. Puedes resumir esto siguiendo estas reglas:
- Solo desea ver
try
/catch
cuándo puede hacer algo con respecto a una excepción específica - Casi nunca querrás ver un código sin formato
new
odelete
en código. - Evite
std::sprintf
,snprintf
y matrices en general: utilícelasstd::ostringstream
para formatear y reemplazar matrices constd::vector
ystd::string
- En caso de duda, busque la funcionalidad en Boost o STL antes de lanzar la suya propia.
Sólo puedo recomendarte que aprendas a usar las excepciones correctamente y te olvides de los códigos de resultados si planeas escribir en C++. Si desea evitar excepciones, puede considerar escribir en otro idioma que no las tenga o que las haga seguras . Si realmente desea aprender cómo utilizar C++ por completo, lea algunos libros de Herb Sutter , Nicolai Josuttis y Scott Meyers .
No es posible escribir código seguro para excepciones bajo el supuesto de que "cualquier línea puede arrojar". El diseño de código seguro para excepciones depende fundamentalmente de ciertos contratos/garantías que se supone que usted 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.
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 sólo una cuestión de codificación simple .