c++ - try - ¿Usted(realmente) escribe código seguro de excepción?
try catch++ (13)
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.
El manejo de excepciones (EH, por sus siglas en inglés) parece ser el estándar actual y, al buscar en la web, no puedo encontrar ideas ni métodos nuevos que intenten mejorarlo o reemplazarlo (bueno, existen algunas variaciones, pero no hay nada nuevo).
Aunque la mayoría de la gente parece ignorarlo o simplemente aceptarlo, EH tiene algunos inconvenientes enormes: las excepciones son invisibles al código y crean muchos, muchos puntos de salida posibles. Joel en software escribió un artículo al respecto . La comparación con goto
encaja perfectamente, me hizo pensar otra vez sobre 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 tiene que escribir un 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 devolver 0 (como en los días anteriores). Esto hace que casi 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 cuidar las excepciones!
Pero es difícil, es muy difícil. Debe aprender a escribir un código de excepción de seguridad, e incluso si tiene alguna experiencia con él, se le pedirá que vuelva a verificar cualquier línea de código para estar seguro. O empiezas a poner bloques try / catch en todas partes, lo que desordena 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áciles de resolver, con un enfoque que crea muchos puntos de salida posibles en su código, y si comienza a escribir código que detecte excepciones están obligados a hacerlo en algún momento), entonces incluso crea una multitud de rutas a través de su código (código en los bloques de captura, piense en un programa de servidor donde necesita instalaciones de registro distintas de std :: cerr ..). EH tiene ventajas, pero ese no es el punto.
Mis preguntas reales:
- ¿Realmente escribe código de excepción de seguridad?
- ¿Está seguro de que su último código "listo para producción" es una excepción segura?
- ¿Puedes estar seguro de que lo es?
- ¿Conoces y / o usas alternativas que funcionan?
Algunos de nosotros preferimos lenguajes como Java que nos obligan a declarar todas las excepciones lanzadas por métodos, en lugar de hacerlas invisibles como en C ++ y C #.
Cuando se hace correctamente, las excepciones son superiores a los códigos de retorno de error, si no por otra razón que no sea necesario propagar los fallos 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 código de control de excepciones limpio en C ++. new(nothrow)
usando mucho new(nothrow)
mucho.
Aunque realmente me gusta trabajar con Eclipse y Java (nuevo en Java), porque arroja errores en el editor si le falta un controlador de 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.
Dejando a un lado la confusión entre las excepciones de SEH y C ++, debe tener en cuenta que se pueden lanzar excepciones en cualquier momento, y escribir su código con eso en mente. La necesidad de seguridad excepcional 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 el código de excepción segura no es particularmente difícil y, de hecho, es más fácil que escribir código que maneje las devoluciones de errores correctamente en todos los casos.
EH es bueno, en general. Pero la implementación de C ++ no es muy amigable, ya que es realmente difícil saber qué tan buena es la cobertura de captura de excepciones. Java, por ejemplo, lo hace fácil, el compilador tenderá a fallar si no maneja las posibles excepciones.
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 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.
¿Realmente escribe código de excepción de seguridad?
Sí. Me esfuerzo por la excepción de código seguro en todos los casos. Evangelizo utilizando técnicas RAII para el acceso de ámbito a los recursos (por ejemplo, boost::shared_ptr
para la memoria, boost::lock_guard
para el bloqueo). En general, el uso coherente de RAII y las técnicas de protección del alcance harán que el código seguro de excepción sea mucho más fácil de escribir. El truco es aprender lo que existe y cómo aplicarlo.
¿Está seguro de que su último código "listo para producción" es una excepción segura?
No. Es tan seguro como lo 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 seguridad excepcional, las técnicas anteriores garantizan la corrección de una manera que es casi imposible de lograr con catch
bloques try
/ catch
. Si está capturando todo en su ámbito de control superior (subproceso, 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 frente a las excepciones sin bloques try
/ catch
todas partes .
¿Puedes incluso estar seguro de que lo es?
Sí. Usted puede estar seguro mediante una auditoría exhaustiva del código, pero nadie realmente hace eso, ¿verdad? Sin embargo, las revisiones regulares de código y los cuidadosos desarrolladores hacen un largo camino para llegar allí.
¿Conoces y / o usas alternativas que funcionan?
He intentado algunas variaciones a lo largo de los años, como los estados de codificación en los bits superiores (como HRESULTs ) o ese horrible setjmp() ... longjmp()
hack. Ambos se dividen en la práctica, aunque de maneras completamente diferentes.
Al final, si adquiere el hábito de aplicar algunas técnicas y pensar con cuidado dónde puede hacer algo en respuesta a una excepción, terminará con un código muy legible que es excepcionalmente seguro. Puedes resumir esto siguiendo estas reglas:
- Solo desea ver
try
/catch
cuando puede hacer algo con respecto a una excepción específica - Casi nunca querrá ver un código
new
odelete
en el código - Evite
std::sprintf
,snprintf
y las matrices en general: usestd::ostringstream
para formatear y reemplazar las matrices porstd::vector
ystd::string
- En caso de duda, busque la funcionalidad en Boost o STL antes de lanzar su propia
Solo puedo recomendar que aprenda a usar las excepciones correctamente y 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 que las haga seguras . Si realmente desea aprender a utilizar C ++ completamente, lea algunos libros de Herb Sutter , Nicolai Josuttis y Scott Meyers .
Escribir código de excepción segura en C ++ no implica tanto usar muchos bloques try {} catch {}. Se trata de documentar qué tipo de garantías proporciona su código.
Recomiendo leer la serie Gurú de la semana de Herb Sutter, en particular las entregas 59, 60 y 61.
Para resumir, hay tres niveles de seguridad de excepción que puede proporcionar:
- Básico: cuando su código lanza una excepción, su código no pierde recursos y los objetos siguen siendo destructibles.
- Fuerte: cuando su código lanza una excepción, deja el estado de la aplicación sin cambios.
- No lanzar: tu 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 una excepción segura.
Intento lo mejor que puedo para escribir un código de excepción segura, sí.
Eso significa que me cuido de vigilar qué líneas pueden tirar. No todos pueden, y es sumamente importante tener eso en cuenta. La clave es realmente pensar y diseñar su código para satisfacer las garantías de excepción definidas en la norma.
¿Se puede escribir esta operación para proporcionar la fuerte garantía de excepción? ¿Tengo que conformarme con el básico? ¿Qué líneas pueden generar excepciones y cómo puedo asegurarme de que si lo hacen, no dañen el objeto?
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 de buen comportamiento. Demasiado ansioso por entrar en pánico, pero seguro.
Debe cometer errores de forma activa en los manejadores para obtener algo inseguro, y solo catch (...) {} se comparará con ignorar el código de error.
No es posible escribir código de excepción segura bajo el supuesto de que "cualquier línea puede lanzar". El diseño del código de excepción segura se basa fundamentalmente en ciertos contratos / garantías que debe esperar, observar, seguir e implementar en su código. Es absolutamente necesario tener un código que esté garantizado para nunca lanzar. Hay otros tipos de garantías de excepción por ahí.
En otras palabras, la creación de un código de excepción segura es, en gran medida, una cuestión de diseño de programas , no solo una cuestión de codificación simple.
Su pregunta hace una afirmación de que "Escribir código de excepción segura es muy difícil". Primero responderé a sus preguntas y luego responderé a la pregunta oculta detrás de ellas.
Respondiendo preguntas
¿Realmente escribe código de excepción de seguridad?
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 de RAII), pero estoy divagando: esta es una pregunta de C ++.
De hecho, es necesario cuando necesita trabajar con STL o el código Boost. Por ejemplo, los subprocesos de C ++ ( boost::thread
o std::thread
) lanzarán una excepción para salir con gracia.
¿Está seguro de que su último código "listo para producción" es una excepción segura?
¿Puedes estar seguro de que lo es?
Escribir código de excepción segura es como escribir código libre de errores.
No puede estar 100% seguro de que su código es seguro de excepción. Pero entonces, se esfuerza por lograrlo, utilizando patrones conocidos y evitando los anti-patrones conocidos.
¿Conoces y / o usas alternativas que funcionan?
No hay alternativas viables en C ++ (es decir, tendrá que volver a C y evitar las bibliotecas de C ++, así como sorpresas externas como Windows SEH).
Código seguro de escritura de excepción
Para escribir el código de excepción de seguridad, primero debe saber qué nivel de seguridad de excepción es cada instrucción que escribe.
Por ejemplo, una new
puede lanzar una excepción, pero la asignación de una función incorporada (por ejemplo, un int o un puntero) no fallará. Un intercambio nunca fallará (nunca escriba un intercambio de lanzamiento), un std::list::push_back
puede lanzar ...
Garantía de excepción
Lo primero que debe comprender es que debe poder evaluar la garantía de excepción ofrecida por todas sus funciones:
- ninguno : tu 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 debe ofrecer al menos, es decir, si se lanza una excepción, no se filtran recursos y todos los objetos están completos.
- fuerte : el procesamiento tendrá éxito, o lanzará una excepción, pero si se produce, los datos estarán en el mismo estado que si el procesamiento no se hubiera iniciado en absoluto (esto le da un poder transaccional a C ++)
- nothrow / nofail : El procesamiento tendrá éxito.
Ejemplo de código
El siguiente código parece correcto en C ++, pero en verdad, ofrece la garantía de "ninguna", 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 se debe hacer es hacer que la función sea "básica", es decir, poner x en un puntero inteligente hasta que la lista sea de su propiedad:
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". Nada se perderá, y todos los objetos estarán en un estado correcto. Pero podríamos ofrecer más, es decir, la fuerte garantía. Aquí es donde puede llegar a ser costoso, y es por esto que no todo el código de C ++ es fuerte. 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 creamos y configuramos X
a su valor correcto. Si falla alguna operación, entonces t
no se modifica, por lo tanto, la operación 1 a 3 puede considerarse "fuerte": si algo se lanza, t
no se modifica y X
no se filtrará porque es propiedad del puntero inteligente.
Luego, creamos una copia t2
de t
, y trabajamos en esta copia de la operación 4 a 7. Si algo se lanza, t2
se modifica, pero t
sigue siendo el original. Seguimos ofreciendo la fuerte garantía.
Luego, intercambiamos t
y t2
. Las operaciones de intercambio no deben realizarse en C ++, por lo que esperemos que el intercambio que escribió para T
se vea (si no lo está, vuelva a escribirlo para que no se vea).
Entonces, si llegamos al final de la función, todo tuvo éxito (no se necesita un tipo de retorno) y t
tiene su valor de excepción. Si falla, entonces t
tiene todavía su valor original.
Ahora, ofrecer la garantía sólida puede 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 un costo (y la integración de C ++ y otras optimizaciones podrían hacer que todo el código anterior sea gratuito) , entonces hacerlo. La función de usuario te lo agradecerá.
Conclusión
Se necesita algún hábito para escribir código de excepción segura. Deberá evaluar la garantía ofrecida por cada instrucción que usará, 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 doxygen @warning), lo cual es un poco triste, pero no debería impedirle intentar escribir un código de excepción segura.
Fallo normal vs. error
¿Cómo puede un programador garantizar que una función de no falla siempre tendrá é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 entonces, en cualquier idioma, llamar a una función supone que la función está libre de errores. Ningún código sano 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 fallos 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 ventaja. 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 / retrotracción, siendo la confirmación la ejecución normal del código, con excepciones como la reversión.
Entonces, el "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 a un recolector de basura le resultaría difícil ofrecer para la base de datos, la conexión o los manejadores de archivos).
Así que, por lo que veo, vale la pena.
Edit 2010-01-29: Sobre swap no lanzable
nobar hizo un comentario que creo que es bastante relevante, porque forma parte de "cómo se escribe un código de excepción segura":
- [yo] Un intercambio nunca fallará (ni siquiera escriba un intercambio de lanzamiento)
- [nobar] Esta es una buena recomendación para las funciones
swap()
escritas a medida. Sin embargo, se debe tener en cuenta questd::swap()
puede fallar en función de las operaciones que utiliza internamente.
el std::swap
predeterminado std::swap
hará copias y asignaciones que, para algunos objetos, pueden lanzar. Por lo tanto, el swap por defecto podría lanzar, ya sea utilizado para sus clases o incluso para las clases STL. En lo que respecta al estándar de C ++, la operación de intercambio por vector
, deque
y list
no se lanzará, mientras que podría map
por map
si el functor de comparación puede lanzar en la construcción de copia (consulte el Lenguaje de Programación de C ++, Edición Especial, apéndice E , E.4.3.Swap ).
Si observamos la implementación del intercambio del vector por parte de Visual C ++ 2008, el intercambio del vector no se lanzará si los dos vectores tienen el mismo asignador (es decir, el caso normal), sino que harán copias si tienen asignadores diferentes. Y así, supongo que podría arrojar en este último caso.
Por lo tanto, el texto original sigue siendo válido: nunca escriba un intercambio de lanzamientos, pero el comentario de nobar debe ser recordado: asegúrese de que los objetos que está intercambiando no tengan un intercambio de lanzamientos.
Edición 2011-11-06: artículo interesante
Dave Abrahams , quien nos dio las garantías básicas / sólidas / de no emisión , describió en un artículo su experiencia sobre la seguridad de la excepción STL:
http://www.boost.org/community/exception_safety.html
Mire el punto 7 (Pruebas automatizadas para la seguridad excepcional), donde confía en las pruebas unitarias automatizadas para asegurarse de que se evalúen 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
t.integer += 1;
¡Sin la garantía de que el desbordamiento no se producirá NO es una excepción segura, y de hecho puede invocar técnicamente a 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 no está firmado el entero no se desborda, pero realiza 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 hacer la suma.
Mi error iría en la sección "Fallo normal contra error", es decir, un error. No invalida el razonamiento, y no significa que el código de excepción segura sea inútil porque es imposible de alcanzar. No puede protegerse contra la desconexión de la computadora, los errores del compilador, o incluso los errores u otros errores. No puedes alcanzar la perfección, pero puedes intentar acercarte lo más posible.
Corrigí el código con el comentario de Dionadar en mente.
- ¿Realmente escribe código de excepción de seguridad?
Bueno, ciertamente tengo la intención de hacerlo.
- ¿Está seguro de que su último código "listo para producción" es una excepción segura?
Estoy seguro de que mis servidores 24/7 creados con excepciones se ejecutan 24/7 y no pierden memoria.
- ¿Puedes estar seguro de que lo es?
Es muy difícil estar seguro de que cualquier código es correcto. Típicamente, solo se puede ir por resultados.
- ¿Conoces y / o usas alternativas que funcionan?
No. Usar excepciones es más limpio y fácil que cualquiera de las alternativas que he usado en los últimos 30 años en la programación.
¿Realmente escribe código de excepción de seguridad? [No hay tal cosa. Las excepciones son un escudo de papel para los errores a menos que tenga un entorno administrado. Esto se aplica a las tres primeras preguntas.]
¿Conoces y / o usas alternativas que funcionan? [¿Alternativa a qué? El problema aquí es que las personas no separan los errores reales de la operación normal del programa. Si es una operación normal del programa (es decir, un archivo no encontrado), no es realmente un manejo de errores. Si es un error real, no hay manera de ''manejarlo'' o no es un error real. Su objetivo aquí es descubrir qué fue lo que falló y detener la hoja de cálculo y registrar un error, reiniciar el controlador de su tostadora, o simplemente rezar para que el avión de combate pueda continuar volando incluso cuando el software tenga errores y espere lo mejor.]