La excepción de isocpp.org afirma que las preguntas frecuentes
No utilice throw para indicar un error de codificación en el uso de una función. Utilice el aserción u otro mecanismo para enviar el proceso a un depurador o para bloquear el proceso y recopilar el volcado de bloqueo para que el desarrollador lo depure.
Por otro lado, la biblioteca estándar define std :: logic_error y todos sus derivados, que me parecen que deben manejar, además de otras cosas, los errores de programación. ¿Pasar una cadena vacía a std :: stof (arrojará inválido_argumento) no es un error de programación? ¿Pasar una cadena que contiene caracteres diferentes de '1' / '0' a std :: bitset (arrojará inválido_argumento) no es un error de programación? ¿Llamar a std :: bitset :: set con un índice no válido (arrojará out_of_range) no es un error de programación? Si no lo son, ¿cuál es un error de programación que uno probaría? El constructor basado en cadenas std :: bitset solo existe desde C ++ 11, por lo que debería haber sido diseñado con el uso idiomático de excepciones en mente. Por otro lado, he tenido gente que me dice que logic_error básicamente no debería usarse en absoluto.
Otra regla que aparece con frecuencia con excepciones es "usar solo excepciones en circunstancias excepcionales". Pero, ¿cómo se supone que una función de biblioteca sabe qué circunstancias son excepcionales? Para algunos programas, no poder abrir un archivo puede ser excepcional. Para otros, no poder asignar memoria puede no ser excepcional. Y hay cientos de casos intermedios. ¿Ser incapaz de crear un socket? ¿No puede conectarse o escribir datos en un socket o un archivo? ¿No se puede analizar la entrada? Podría ser excepcional, podría no serlo. La función en sí misma definitivamente no puede saber en general, no tiene idea de en qué tipo de contexto se llama.
Entonces, ¿cómo se supone que debo decidir si debo usar excepciones o no para una función en particular? Me parece que la única forma realmente consistente es usarlos para el manejo de todos y cada uno de los errores, o para nada. Y si estoy usando la biblioteca estándar, esa elección fue hecha por mí.
fuente
Respuestas:
Primero, me siento obligado a señalar que
std::exception
y sus hijos fueron diseñados hace mucho tiempo. Hay una serie de partes que probablemente (casi con certeza) serían diferentes si se estuvieran diseñando hoy.No me malinterpreten: hay partes del diseño que han funcionado bastante bien, y son muy buenos ejemplos de cómo diseñar una jerarquía de excepción para C ++ (por ejemplo, el hecho de que, a diferencia de la mayoría de las otras clases, todas comparten un raíz común).
Mirando específicamente
logic_error
, tenemos un poco de enigma. Por un lado, si tiene alguna opción razonable en el asunto, el consejo que citó es correcto: generalmente es mejor fallar tan rápido y ruidosamente como sea posible para que pueda ser depurado y corregido.Sin embargo, para bien o para mal, es difícil definir la biblioteca estándar en torno a lo que generalmente debe hacer. Si definió estos para salir del programa (por ejemplo, llamar
abort()
) cuando se le da una entrada incorrecta, eso sería lo que siempre sucedió para esa circunstancia, y en realidad hay bastantes circunstancias bajo las cuales esto probablemente no sea realmente lo correcto. , al menos en código desplegado.Eso se aplicaría en código con requisitos (al menos suaves) en tiempo real y una penalización mínima por una salida incorrecta. Por ejemplo, considere un programa de chat. Si está decodificando algunos datos de voz y recibe alguna entrada incorrecta, es probable que un usuario esté mucho más feliz de vivir con un milisegundo de estática en la salida que un programa que simplemente se apaga por completo. Del mismo modo, cuando se realiza la reproducción de video, puede ser más aceptable vivir produciendo valores incorrectos para algunos píxeles para un cuadro o dos que hacer que el programa salga sumariamente porque la secuencia de entrada se corrompió.
En cuanto a si se deben usar excepciones para informar ciertos tipos de errores: tiene razón: la misma operación podría calificar como una excepción o no, dependiendo de cómo se esté utilizando.
Por otro lado, también está equivocado: el uso de la biblioteca estándar no (necesariamente) obliga a tomar esa decisión. En el caso de abrir un archivo, normalmente estaría usando un iostream. Iostreams tampoco es exactamente el último y mejor diseño, pero en este caso hacen las cosas bien: le permiten configurar un modo de error, por lo que puede controlar si no se abre un archivo con el resultado de una excepción o no. Por lo tanto, si tiene un archivo que es realmente necesario para su aplicación, y no abrirlo significa que debe tomar algunas medidas correctivas serias, entonces puede hacer que arroje una excepción si no puede abrir ese archivo. Para la mayoría de los archivos, que intentará abrir, si no existen o no son accesibles, simplemente fallarán (este es el valor predeterminado).
En cuanto a cómo decides: no creo que haya una respuesta fácil. Para bien o para mal, las "circunstancias excepcionales" no siempre son fáciles de medir. Si bien hay casos que son fáciles de decidir, deben ser [un] excepcionales, hay (y probablemente siempre lo serán) casos en los que está abierto a dudas o requiere un conocimiento del contexto que está fuera del dominio de la función en cuestión. Para casos como ese, al menos puede valer la pena considerar un diseño más o menos similar a esta parte de iostreams, donde el usuario puede decidir si la falla resulta en una excepción o no. Alternativamente, es completamente posible tener dos conjuntos separados de funciones (o clases, etc.), una de las cuales arrojará excepciones para indicar una falla, y la otra utiliza otros medios. Si vas por esa ruta,
fuente
Puede que no lo creas, pero, bueno, diferentes codificadores de C ++ no están de acuerdo. Es por eso que las preguntas frecuentes dicen una cosa, pero la biblioteca estándar no está de acuerdo.
Las preguntas frecuentes recomiendan fallar porque será más fácil de depurar. Si se bloquea y obtiene un volcado de núcleo, tendrá el estado exacto de su aplicación. Si lanzas una excepción, perderás mucho de ese estado.
La biblioteca estándar toma la teoría de que dar al codificador la capacidad de detectar y manejar el error es más importante que la depuración.
La idea aquí es que si su función no sabe si la situación es excepcional o no, no debería arrojar una excepción. Debería devolver un estado de error a través de algún otro mecanismo. Una vez que llega a un punto en el programa en el que sabe que el estado es excepcional, debe lanzar la excepción.
Pero esto tiene su propio problema. Si una función devuelve un estado de error, es posible que no recuerde verificarlo y el error pasará silenciosamente. Esto lleva a algunas personas a abandonar las excepciones, son una regla excepcional a favor de lanzar excepciones para cualquier tipo de estado de error.
En general, el punto clave es que diferentes personas tienen diferentes ideas sobre cuándo lanzar excepciones. No vas a encontrar una sola idea coherente. A pesar de que algunas personas afirmarán dogmáticamente que esta o esa es la forma correcta de manejar las excepciones, no existe una única teoría acordada.
Puedes lanzar excepciones:
y encuentre a alguien en Internet que esté de acuerdo con usted. Tendrás que adoptar el estilo que funcione para ti.
fuente
Se han escrito muchas otras buenas respuestas, solo quiero agregar un breve punto.
La respuesta tradicional, especialmente cuando se escribieron las preguntas frecuentes de ISO C ++, compara principalmente "excepción C ++" versus "código de retorno de estilo C". Una tercera opción, "devolver algún tipo de valor compuesto, por ejemplo, a
struct
ounion
, o hoy en día,boost::variant
o (propuesto)std::expected
, no se considera.Antes de C ++ 11, la opción "devolver un tipo compuesto" solía ser muy débil. Debido a que no había semántica de movimiento, copiar cosas dentro y fuera de una estructura era potencialmente muy costoso. Era extremadamente importante en ese punto del lenguaje diseñar su código hacia RVO para obtener el mejor rendimiento. Las excepciones fueron como una manera fácil de devolver de manera efectiva un tipo compuesto, cuando de lo contrario eso sería bastante difícil.
En mi opinión, después de C ++ 11, esta opción "devolver una unión discriminada", similar al idioma
Result<T, E>
utilizado en Rust hoy en día, debería ser más frecuente en el código C ++. A veces es realmente un estilo más simple y más conveniente de indicar errores. Con excepciones, siempre existe la posibilidad de que las funciones que no se lanzaron antes de repente puedan comenzar a lanzarse después de un refactorizador, y los programadores no siempre documentan esas cosas tan bien. Cuando el error se indica como parte del valor de retorno en una unión discriminada, reduce en gran medida la posibilidad de que el programador simplemente ignore el código de error, que es la crítica habitual al manejo de errores de estilo C.Generalmente
Result<T, E>
funciona como un impulso opcional. Puede probar, utilizandooperator bool
, si es un valor o un error. Y luego use sayoperator *
para acceder al valor, o alguna otra función "get". Por lo general, ese acceso no está marcado, por velocidad. Pero puede hacerlo de modo que en una compilación de depuración, el acceso se compruebe y una aserción se asegure de que realmente haya un valor y no un error. De esta manera, cualquiera que no verifique los errores correctamente obtendrá una afirmación difícil en lugar de un problema más insidioso.Una ventaja adicional es que, a diferencia de las excepciones en las que, si no se detecta, simplemente vuela la pila a cierta distancia arbitraria, con este estilo, cuando una función comienza a indicar un error donde no lo había hecho antes, no puede compilar a menos que el el código se cambia para manejarlo. Esto aumenta los problemas: el problema tradicional de la "excepción no detectada" se parece más a un error en tiempo de compilación que a un error en tiempo de ejecución.
Me he convertido en un gran admirador de este estilo. Por lo general, hoy en día uso esto o excepciones. Pero trato de limitar las excepciones a problemas mayores. Para algo así como un error de análisis, intento volver,
expected<T>
por ejemplo. Cosas comostd::stoi
yboost::lexical_cast
que arrojan una excepción de C ++ en caso de algún problema relativamente menor "la cadena no se puede convertir en número" me parecen de muy mal gusto hoy en día.fuente
std::expected
sigue siendo una propuesta no aceptada, ¿verdad?exception_ptr
, o simplemente desea usar algún tipo de estructura o algo así? como eso.[[nodiscard]] attribute
será útil para este enfoque de manejo de errores, ya que garantiza que no ignore simplemente el resultado del error por accidente.except_ptr
) había que lanzar una excepción internamente. Personalmente, creo que dicha herramienta debería funcionar completamente independiente de las ejecuciones. Solo un comentario.Este es un tema muy subjetivo, ya que es parte del diseño. Y debido a que el diseño es básicamente arte, prefiero discutir estas cosas en lugar de debatir (no estoy diciendo que estés debatiendo).
Para mí, los casos excepcionales son de dos tipos: los que tratan con recursos y los que tratan con operaciones críticas. Lo que puede considerarse crítico depende del problema en cuestión y, en muchos casos, del punto de vista del programador.
La falta de adquisición de recursos es un candidato principal para lanzar excepciones. El recurso puede ser memoria, archivo, conexión de red o cualquier otra cosa según su problema y plataforma. Ahora, ¿el hecho de no liberar un recurso garantiza una excepción? Bueno, eso nuevamente depende. No he hecho nada en lo que falló la liberación de memoria, así que no estoy seguro de ese escenario. Sin embargo, la eliminación de archivos como parte de la liberación de recursos puede fallar, y ha fallado para mí, y esa falla generalmente está vinculada a otro proceso que la ha mantenido abierta en una aplicación multiproceso. Supongo que otros recursos podrían fallar durante el lanzamiento como podría hacerlo un archivo, y generalmente es la falla de diseño lo que provoca este problema, por lo que solucionarlo sería mejor que lanzar una excepción.
Luego viene la actualización de recursos. Este punto, al menos para mí, está estrechamente relacionado con el aspecto de operaciones críticas de la aplicación. Imagine una
Employee
clase con una funciónUpdateDetails(std::string&)
que modifica los detalles en función de una cadena separada por comas dada. Similar a la falla de la liberación de memoria, me resulta difícil imaginar que la asignación de valores de las variables miembro falla debido a mi falta de experiencia en los dominios en los que esto podría suceder. Sin embargo,UpdateDetailsAndUpdateFile(std::string&)
se espera que falle una función como la que hace como su nombre indica. Esto es lo que yo llamo operación crítica.Ahora, debe ver si la llamada operación crítica justifica una excepción. Quiero decir, ¿se está actualizando el archivo al final, como en el destructor, o es simplemente una llamada paranoica realizada después de cada actualización? ¿Existe un mecanismo alternativo que escriba objetos no escritos regularmente? Lo que digo es que hay que evaluar la importancia de la operación.
Obviamente, hay muchas operaciones críticas que no están vinculadas a los recursos. Si
UpdateDetails()
se le dan datos incorrectos, no actualizará los detalles y la falla debe darse a conocer, por lo que arrojaría una excepción aquí. Pero imagina una función comoGiveRaise()
. Ahora, si dicho empleado tiene la suerte de tener un jefe de pelo puntiagudo y no obtendrá un aumento (en términos de programación, el valor de alguna variable evita que esto suceda), la función esencialmente ha fallado. ¿Lanzarías una excepción aquí? Lo que digo es que tienes que evaluar la necesidad de una excepción.Para mí, la coherencia es en términos de mi enfoque de diseño que la usabilidad de mis clases. Lo que quiero decir es que no pienso en términos de "todas las funciones de Get deben hacer esto y todas las funciones de Actualización deben hacerlo", sino ver si una función en particular apela a cierta idea dentro de mi enfoque. En su superficie, las clases podrían parecer un poco "al azar", pero cada vez que los usuarios (en su mayoría colegas de otros equipos) despotrican o preguntan al respecto, se lo explicaré y parecerán satisfechos.
Veo a muchas personas que básicamente reemplazan los valores de retorno con excepciones porque están usando C ++ y no C, y eso me da una 'separación agradable del manejo de errores', etc., y me insta a dejar de 'mezclar' idiomas, etc. ese tipo de personas.
fuente
En primer lugar, como otros han dicho, las cosas no son que claro corte en C ++, en mi humilde opinión sobre todo debido a los requisitos y las restricciones son algo más variado en C ++ que otros lenguajes, esp. C # y Java, que tienen problemas de excepción "similares".
Expondré en el ejemplo std :: stof:
El contrato básico , tal como lo veo, de esta función es que intenta convertir su argumento en flotante, y cualquier falla al hacerlo se informa por una excepción. Ambas posibles excepciones se derivan,
logic_error
pero no en el sentido de error del programador, sino en el sentido de que "la entrada no se puede convertir en flotante".Aquí, uno puede decir que
logic_error
se usa a para indicar que, dada esa entrada (tiempo de ejecución), siempre es un error intentar convertirlo, pero es el trabajo de la función determinar eso y decírselo (a través de una excepción).Nota al margen: en esa vista, a
runtime_error
podría verse como algo que, dada la misma entrada a una función, teóricamente podría tener éxito para diferentes ejecuciones. (por ejemplo, una operación de archivo, acceso a base de datos, etc.)Nota adicional: La biblioteca de expresiones regulares de C ++ eligió derivar su error,
runtime_error
aunque hay casos en los que podría clasificarse de la misma manera que aquí (patrón de expresiones regulares no válido).Esto solo muestra, en mi humilde opinión, que la agrupación
logic_
o elruntime_
error es bastante confuso en C ++ y realmente no ayuda mucho en el caso general (*): si necesita manejar errores específicos, probablemente necesite atrapar más bajo que los dos.(*): Eso no quiere decir que una sola pieza de código no debe ser consistente, pero si usted lanza
runtime_
ologic_
ocustom_
tantos no es realmente tan importante, creo.Para comentar sobre ambos
stof
ybitset
:Ambas funciones toman cadenas como argumento, y en ambos casos es:
Esta declaración tiene, en mi humilde opinión, dos raíces:
Rendimiento : si se llama a una función en una ruta crítica, y el caso "excepcional" no es excepcional, es decir, una cantidad significativa de pases implicará lanzar una excepción, entonces pagar cada vez por la maquinaria de desenrollado de excepción no tiene sentido , y puede ser demasiado lento.
Localidad del manejo de errores : si se invoca una función y la excepción se captura y procesa de inmediato, entonces tiene poco sentido lanzar una excepción, ya que el manejo de errores será más detallado con el
catch
que con unif
.Ejemplo:
Aquí es donde entran en juego funciones como
TryParse
vsParse
.: una versión para cuando el código local espera que la cadena analizada sea válida, una versión cuando el código local supone que realmente se espera (es decir, no excepcional) que el análisis falle.De hecho,
stof
es solo (definido como) un contenedorstrtof
, así que si no quieres excepciones, usa esa.En mi humilde opinión, tienes dos casos:
Función similar a "Biblioteca" (reutilizada a menudo en diferentes contextos): Básicamente no puede decidir. Posiblemente proporcione ambas versiones, tal vez una que informe un error y una envoltura que convierta el error devuelto en una excepción.
La función "Aplicación" (específica para un blob de código de aplicación, puede reutilizarse en parte, pero está restringida por el estilo de manejo de errores de las aplicaciones, etc.): Aquí, a menudo debería ser bastante claro. Si la (s) ruta (s) de código que llama a las funciones manejan las excepciones de una manera sensata y útil, use las excepciones para informar cualquier error (pero vea a continuación) . Si el código de la aplicación se lee y escribe más fácilmente para un estilo de retorno de error, utilícelo por todos los medios.
Por supuesto, habrá lugares intermedios, solo use lo que necesita y recuerde YAGNI.
Por último, creo que debería volver a la declaración de preguntas frecuentes,
Me suscribo a esto para todos los errores que son una clara indicación de que algo está muy mal o que el código de llamada claramente no sabía lo que estaba haciendo.
Pero cuando esto es apropiado, a menudo es altamente específico de la aplicación, por lo tanto, consulte el dominio de biblioteca anterior vs.
Esto recae en la pregunta sobre si y cómo validar las condiciones previas de llamada , pero no voy a entrar en eso, responder ya demasiado tiempo :-)
fuente