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
¿Realmente escribes código seguro de excepción?
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
o std::thread
) generarán una excepción para salir con gracia.
¿Está seguro de que su último código "listo para producción" es seguro para excepciones?
¿Puedes estar seguro de que lo es?
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.
¿Conoces y / o utilizas alternativas que funcionan?
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), un std::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:
- ninguno : su código nunca debe 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 aún están completos
- fuerte : el procesamiento tendrá éxito o arrojará una excepción, pero si arroja, entonces los datos estarán en el mismo estado que si el procesamiento no hubiera comenzado 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 C ++, pero en verdad, 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 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:
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 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:
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 configurando X
su valor correcto. Si alguna operación falla, entonces t
no se modifica, por lo tanto, la operación 1 a 3 puede considerarse "fuerte": si algo arroja, 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 arroja, t2
se modifica, pero entonces, t
sigue siendo el original. Todavía ofrecemos la garantía fuerte.
Luego, intercambiamos t
y t2
. Las operaciones de intercambio no deben arrojarse en C ++, por lo tanto, esperemos que el intercambio para el que escribió no T
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":
- [yo] Un intercambio nunca fallará (ni siquiera escriba un intercambio de lanzamiento)
- [nobar] Esta es una buena recomendación para
swap()
funciones escritas a medida . Cabe señalar, sin embargo, que std::swap()
puede fallar en función de las operaciones que utiliza internamente
el 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 para vector
, deque
y list
no arrojará, mientras que podría hacerlo map
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
t.integer += 1;
es sin la garantía de que el desbordamiento no sucederá NO es una excepción segura, y de hecho técnicamente puede invocar a UB. (El desbordamiento firmado es UB: C ++ 11 5/4 "Si durante la evaluación de una expresión, el resultado no está matemáticamente definido o no está en el rango de valores representables para su tipo, el comportamiento no está definido".) Tenga en cuenta que sin signo los enteros no se desbordan, pero hacen 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 "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.