C ++ – Argumentos para excepciones sobre códigos de retorno

Tengo una discusión sobre qué camino tomar en un nuevo proyecto de C ++. Estoy a favor de las excepciones sobre los códigos de retorno (solo para casos excepcionales) por las siguientes razones:

  1. Los constructores no pueden dar un código de retorno
  2. Desacopla la ruta de falla (que debería ser muy rara) del código lógico que es más limpio
  3. Más rápido en el caso no excepcional (sin verificar si / sino cientos de miles de veces)
  4. Si alguien atornilla la configuración del código de retorno (olvida devolver FAIL) puede llevar mucho tiempo localizarlo.
  5. Mejor información del mensaje contenido en el error. (Se me indicó que una enumeración de retorno podría hacer lo mismo para los códigos de error)
  6. De Jared Par Imposible ignorar sin código específicamente diseñado para manejarlo

Estos son los puntos que he pensado al pensar en ellos y en las búsquedas de Google. Debo admitir que estoy predispuesto a que las excepciones hayan funcionado en C # durante los últimos años. Por favor, publique otras razones para usar excepciones sobre códigos de retorno. Para aquellos que prefieren códigos de retorno, también estaría dispuesto a escuchar su razonamiento. Gracias

Creo que este artículo lo resume.

Argumentos para usar excepciones

  1. Las excepciones separan el código de manejo de errores del flujo de progtwig normal y, por lo tanto, hacen que el código sea más legible, robusto y extensible.
  2. Lanzar una excepción es la única forma limpia de informar un error desde un constructor.
  3. Las excepciones son difíciles de ignorar, a diferencia de los códigos de error.
  4. Las excepciones se propagan fácilmente desde funciones profundamente anidadas.
  5. Las excepciones pueden ser, y a menudo son, tipos definidos por el usuario que llevan mucha más información que un código de error.
  6. Los objetos de excepción se combinan con los controladores mediante el sistema de tipos.

Argumentos contra el uso de excepciones

  1. Las excepciones rompen la estructura del código al crear múltiples puntos de salida invisibles que hacen que el código sea difícil de leer e inspeccionar.
  2. Las excepciones conducen fácilmente a fugas de recursos, especialmente en un lenguaje que no tiene recolector de basura incorporado y finalmente bloquea.
  3. Aprender a escribir código de excepción es difícil.
  4. Las excepciones son costosas y rompen la promesa de pagar solo por lo que usamos.
  5. Las excepciones son difíciles de introducir en el código heredado.
  6. Las excepciones se abusan fácilmente para realizar tareas que pertenecen al flujo normal del progtwig.

El mejor caso que he escuchado para preferir códigos de retorno a excepciones es simplemente esto:

  1. Escribir código de excepción segura es difícil [en C ++].

Con una gran cantidad de experiencia reciente en C #, puedo identificarme con tu deseo de usar excepciones, pero desafortunadamente C ++ no es C #, y muchas de las cosas que podemos evitar en C # pueden ser finalmente mortales en C ++.

Puede encontrar un buen resumen del caso a favor y en contra en las pautas de estilo de Google . En breve:

Pros:

  • Las excepciones permiten que los niveles más altos de una aplicación decidan cómo manejar las fallas de “no puede suceder” en funciones profundamente anidadas, sin la oscurecimiento y la contabilidad propensa a errores de los códigos de error.
  • Las excepciones son utilizadas por la mayoría de los otros idiomas modernos. Usarlos en C ++ lo haría más consistente con Python, Java y el C ++ con el que otros están familiarizados.
  • Algunas bibliotecas de C ++ de terceros usan excepciones, y apagarlas internamente dificulta la integración con esas bibliotecas.
  • Las excepciones son la única forma de que un constructor falle. Podemos simular esto con una función de fábrica o un método Init (), pero estos requieren la asignación de un montón o un nuevo estado “inválido”, respectivamente.
  • Las excepciones son realmente útiles en los marcos de prueba.

Contras:

  • Cuando agrega una instrucción throw a una función existente, debe examinar todas sus llamadas transitivas. O bien deben hacer al menos la garantía de seguridad de excepción básica, o nunca deben atrapar la excepción y estar contentos con el progtwig que termina como resultado. Por ejemplo, si f () llama a g () llama a h (), y h arroja una excepción que f atrapa, g tiene que tener cuidado o puede no limpiarse correctamente.
  • De manera más general, las excepciones hacen que el flujo de control de los progtwigs sea difícil de evaluar mirando el código: las funciones pueden regresar en lugares que no se esperan. Esto da lugar a dificultades de mantenimiento y eliminación de fallas. Puede minimizar este costo mediante algunas reglas sobre cómo y dónde se pueden usar las excepciones, pero a costa de más de lo que un desarrollador necesita saber y comprender.
  • La seguridad de las excepciones requiere RAII y diferentes prácticas de encoding. Se necesita una gran cantidad de maquinaria de apoyo para que la escritura sea correcta y fácil. Además, para evitar requerir que los lectores entiendan todo el gráfico de llamadas, el código de excepción segura debe aislar la lógica que escribe en estado persistente en una fase de “confirmación”. Esto tendrá beneficios y costos (tal vez cuando se vea obligado a ofuscar el código para aislar la confirmación). Permitir excepciones nos obligaría a pagar siempre esos costos incluso cuando no valen la pena.
  • Activar las excepciones agrega datos a cada binario producido, aumentando el tiempo de comstackción (probablemente un poco) y posiblemente aumentando la presión de espacio de direcciones.
  • La disponibilidad de excepciones puede alentar a los desarrolladores a lanzarlas cuando no sean apropiadas o recuperarse de ellas cuando no sea seguro hacerlo. Por ejemplo, la entrada de usuario no válida no debe provocar excepciones. ¡Tendríamos que hacer que la guía de estilo sea aún más larga para documentar estas restricciones!

Sugiero leer y comprender los pros y los contras, luego tomar una decisión para su propio proyecto basado en estos. No tiene el mismo software que Google, por lo que lo que tiene sentido para ellos puede no tener sentido para usted (y es por eso que omití su conclusión).

En mi humilde opinión, la razón número 1 para preferir las excepciones sobre los códigos de retorno es que no puede ignorar silenciosamente una excepción. Hacerlo requiere al menos una cantidad mínima de código adicional.

Use excepciones para condiciones de error excepcionales. Tienes algunos buenos argumentos y me gustaría atacar algunos argumentos en contra.

En primer lugar, la biblioteca estándar de C ++ usa excepciones en todo el lugar. No puede usar clases contenedor o iostreams sin tenerlos presentes. Dado que muchas de las funciones útiles van a usarlos, tratar de llevarnos bien sin ellos va a presentar muchos problemas.

En segundo lugar, no es difícil escribir código de excepción una vez que haya aprendido cómo hacerlo. Requiere RAII consistente, pero así es como debe escribir de todos modos. Debe adoptar un enfoque de compromiso de construcción, pero eso con frecuencia es una ventaja, y evita algunos errores sutiles. (Por ejemplo, el problema de autoasignación desaparece por completo con un enfoque de intercambio de copia). En mi experiencia, el código de excepción segura se ve mejor en general. Es algo que los progtwigdores de C ++ tienen que aprender, pero hay muchas cosas que los progtwigdores de C ++ tienen que aprender, y esto no es mucho más.

En tercer lugar, siempre que limite las excepciones a casos excepcionales, debe haber efectos mínimos sobre el rendimiento . Y, como ha señalado Pavel Minaev, si tiene que volver a pasar los códigos de error con los resultados, existe la posibilidad de efectos en el rendimiento, ya que C ++ no está configurado para retornos fáciles de valores múltiples.

En cuarto lugar, es realmente difícil hacer que el código anterior sea excepcionalmente seguro. Sin embargo, este es un nuevo proyecto.

Entonces, no veo buenas razones para no lanzar excepciones en circunstancias excepcionales, y muchas razones para hacerlo.

Más rápido en el caso no excepcional (sin verificar si / sino cientos de miles de veces)

En el caso no excepcional, es una comparación única para determinar que se devolvió E_SUCCESS.

Si alguien atornilla la configuración del código de retorno (olvida devolver FAIL) puede llevar mucho tiempo localizarlo.

Si alguien no puede verificar las excepciones, puede ser difícil de notar hasta que realmente obtenga una excepción. Si está lidiando con códigos de error, solo lo verá si los está verificando o no.

Usa lo que tiene sentido Creo que ambos tienen un lugar. Hay situaciones en las que los códigos de error son casi imposibles de usar (error de devolución de un constructor, por ejemplo)

Otras veces, los códigos de error son más convenientes. Son más fáciles de tratar en los casos en que esperas que sucedan. Las excepciones son para errores excepcionales , los que se supone que no deben suceder, pero podrían hacerlo una vez en una luna azul. Los códigos de error son mucho más convenientes para los errores que se espera que sucedan regularmente, y se pueden manejar localmente. Las excepciones son más útiles en casos donde el error debe manejarse más arriba en la stack de llamadas.

Además, las excepciones no son necesariamente más rápidas en el caso no excepcional. A menudo, requieren un código adicional de manejo de excepciones en los prólogos y epílogos de funciones, que deben ejecutarse cada vez que se llama a la función, ya sea que arroje una excepción o no.

Como regla general. cuando la recuperación es posible y se espera, utilice códigos de retorno.

Cuando la recuperación no es posible o no se desea , use excepciones.

El manejo de errores es difícil, escribir código limpio con y sin excepciones es difícil.

Como se trata de un proyecto nuevo, no tiene que preocuparse por hacer que la excepción de código antiguo sea segura, sin embargo, debe preocuparse por escribir un código limpio y claro.

Hazlo usando excepciones cuando corresponda.

Como muchos otros ya han proporcionado las razones técnicas para usar excepciones sobre códigos de error, daré una práctica.

Trabajo en un sistema complejo que usa códigos de retorno en lugar de excepciones. Ahora bien, este es un código muy bien diseñado, pero apostaría a que, en promedio, alrededor del 70% del código en cada función es un código de manejo de errores. Una función típica se ve así:

 long Foo( ) { long retCode = MoveStage( ); if ( retCode != RetCode_OK ) { logger->LogError( __LINE__, __FILE__, "some message" ); return( retCode ); } int someConfigVar = 0; long retCode = GetConfigVar( "SomeVarName", someConfigVar ); if ( retCode != RetCode_OK ) { logger->LogError( __LINE__, __FILE__, "some message" ); return( retCode ); } long retCode = DoSomething( ); if ( retCode != RetCode_OK ) { logger->LogError( __LINE__, __FILE__, "some message" ); return( retCode ); } // and on and on and on... } 

El código está lleno de esto y es difícil de seguir. Además de eso, en muchos lugares el código de retorno se ignora por completo porque sabemos que la llamada no fallará. Cada función devuelve un código de ret, por lo que lo que normalmente devolvería es que la salida de la función debe devolverse como un parámetro de salida. Además, todas estas funciones simplemente devuelven el retCode por error, por lo que solo lanzamos ese maldito retCode a la parte superior si ocurre algo malo. No se puede centralizar su estrategia de manejo de errores de esta manera, se convierte en un dolor de cabeza desordenado.

Es muy difícil escribir código de excepción de seguridad. Un ejemplo completamente inventado es:

 void Class::Method() { i++; SomeCallThatMightThrow(); j++; } 

Reemplace i ++ y j ++ con dos variables, recursos, estados que deben permanecer sincrónicos. La recolección de basura nos salvó de tener que recordar emparejar nuestros nuevos y eliminar. Irónicamente, las pruebas de código de retorno explícitas a la antigua nos ahorran tener que analizar cuidadosamente todas las funciones que pueden arrojar excepciones para comprobar que no han atornillado con las condiciones posteriores.

Una de las cosas que me gusta de C ++ es que es muy fácil pensar cómo se podrían implementar las características de nivel superior en términos de características C (que son fáciles de entender en términos de ensamblaje). Las excepciones para C ++ rompen este molde. Para obtener este nivel de comprensión, tengo que hacer mucho. Solo lea esto y pasará mucho tiempo rascándose la cabeza antes de comprenderlo.

Además, las excepciones requieren que tenga una buena disciplina para que su código de excepción sea seguro y libre de fugas de recursos. Esto significa usar RAII para cualquier cosa que contenga un recurso.

Además, se han mostrado excepciones cuando los he medido en muchos, muchos órdenes de magnitud más lentos en comparación con un código de retorno simple.

Bueno, entonces dicen que solo debes lanzar en circunstancias excepcionales, pero ¿cómo se comunican los errores no excepcionales, esperados y que ocurren a menudo? Bueno, devuelve los códigos, por supuesto! 🙂

No veo cómo los beneficios valen la pena.

  1. Los constructores no pueden dar un código de retorno (excepto si posiblemente estás lanzando excepciones en un constructor en el que te encuentras para un mundo de dolor)

  2. Desacopla la ruta de falla (que debería ser muy rara) del código lógico que es más limpio (Abre una ruta de falla mucho más amplia. Las excepciones de C ++ no son como C #. Me encantan las excepciones de C #. Las excepciones de C ++ son inútiles. a la implementación, en parte debido a lo que es y no es C ++)

  3. Más rápido en el caso no excepcional (sin verificar si / sino cientos de miles de veces) (En realidad no. Tiene que verificar el mismo número de errores pase lo que pase, simplemente no ve dónde se comprueban. ¿Son los errores importantes y los que no o al menos importan a su código, por lo que no tiene que comprobar cada cosa en particular (verifica si hay un nuevo error?))
  4. Si alguien atornilla la configuración del código de retorno (olvida devolver FAIL) puede llevar mucho tiempo localizarlo. (Tomaré un millón de esos errores sobre cualquiera de los errores comunes que ocurren con las excepciones de C ++)
  5. Mejor información del mensaje contenido en el error. (Se me indicó que una enumeración de retorno podría hacer lo mismo con los códigos de error) (Bueno, ¿cómo implementa su manejo de errores? Depende de usted, ¿no? Puede tener excepciones que no brinden información útil o códigos de error que desconectan los datos extems en caso de error. En C ++ parece que nunca proporcionan información útil en los casos que realmente desea que lo hagan)
  6. De Jared Par Imposible ignorar sin código específicamente diseñado para manejarlo (Claro, pero este código es generalmente inútil. Es como aprobar una ley que dice que todos deben tener progtwigs libres de errores. Las personas que no obedecen leyes de todos modos y para las personas que ya manejan los errores donde deberían hacerlo)

En cuanto a los motivos en contra:

  1. Querer que tu progtwig realmente funcione al menos parte del tiempo.
  2. actuación.
  3. Ideología. Si es un error, se supone que no debemos ‘recuperarlo’. O bien morimos con un mensaje de error, registramos y continuamos, o lo ignoramos por completo. Si pudiéramos solucionarlo, no sería un error, sino una característica … y si utiliza un código así intencionalmente, ninguna herramienta de depuración en el mundo lo va a salvar.

Mayormente uno.