¿Excepciones como afirmaciones o como errores?

10

Soy un programador profesional de C y un programador aficionado de Obj-C (OS X). Recientemente he tenido la tentación de expandirme a C ++, debido a su sintaxis muy rica.

Hasta ahora, la codificación no he tratado mucho con excepciones. Objective-C los tiene, pero la política de Apple es bastante estricta:

Importante Debe reservar el uso de excepciones para la programación o errores inesperados de tiempo de ejecución, como el acceso a colecciones fuera de límites, intentos de mutar objetos inmutables, enviar un mensaje no válido y perder la conexión con el servidor de Windows.

C ++ parece preferir usar excepciones con más frecuencia. Por ejemplo, el ejemplo de Wikipedia en RAII arroja una excepción si no se puede abrir un archivo. Objective-C lo haría return nilcon un error enviado por un parámetro externo. En particular, parece que std :: ofstream se puede configurar de cualquier manera.

Aquí, en los programadores, he encontrado varias respuestas que proclaman usar excepciones en lugar de códigos de error o no usar excepciones en absoluto . Los primeros parecen más frecuentes.

No he encontrado a nadie haciendo un estudio objetivo para C ++. Me parece que, dado que los punteros son raros, tendría que ir con indicadores de error internos si elijo evitar excepciones. ¿Será demasiado molesto de manejar o tal vez funcionará incluso mejor que las excepciones? Una comparación de ambos casos sería la mejor respuesta.

Editar: aunque no es completamente relevante, probablemente debería aclarar qué niles. Técnicamente es lo mismo NULL, pero la cuestión es que está bien enviar un mensaje a nil. Entonces puedes hacer algo como

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

incluso si la primera llamada regresó nil. Y como nunca lo hace *objen Obj-C, no hay riesgo de una desreferencia de puntero NULL.

Por Johansson
fuente
En mi humilde opinión, sería mejor si mostraras una parte del código del Objetivo C cómo trabajas con los errores allí. Parece que la gente habla de algo diferente de lo que tú preguntas.
Gangnus
En cierto modo, supongo que estoy buscando justificación para usar excepciones. Como tengo una idea de cómo se implementan, sé cuán caros pueden ser. Pero supongo que si puedo obtener un argumento lo suficientemente bueno para su uso, iré por ese camino.
Por Johansson
Parece que para C ++ necesita más bien una justificación para no usarlos . De acuerdo con las reacciones aquí.
Gangnus
2
Quizás, pero hasta ahora no ha habido nadie que explique por qué son mejores (excepto en comparación con los códigos de error). No me gusta el concepto de usar excepciones para cosas que no son excepcionales, pero es más instintivo que basado en hechos.
Por Johansson
"No me gusta ... usar excepciones para cosas que no son excepcionales" - estuvo de acuerdo.
Gangnus

Respuestas:

1

C ++ parece preferir usar excepciones con más frecuencia.

Sugeriría en realidad menos que Objective-C en algunos aspectos porque la biblioteca estándar de C ++ generalmente no arrojaría errores de programador como el acceso fuera de los límites de una secuencia de acceso aleatorio en su forma de diseño de caso más común (en operator[], es decir) o tratando de desreferenciar un iterador inválido. El lenguaje no arroja el acceso a una matriz fuera de los límites, o desreferenciar un puntero nulo, o algo por el estilo.

Eliminar los errores del programador en gran medida de la ecuación de manejo de excepciones en realidad elimina una categoría muy grande de errores a los que otros lenguajes a menudo responden throwing. C ++ tiende a assert(que no se compila en versiones de lanzamiento / producción, solo compilaciones de depuración) o simplemente falla (a menudo se bloquea) en tales casos, probablemente en parte porque el lenguaje no quiere imponer el costo de tales comprobaciones de tiempo de ejecución como se requeriría para detectar dichos errores del programador a menos que el programador específicamente desee pagar los costos escribiendo un código que realice dichos controles por sí mismo.

Sutter incluso recomienda evitar excepciones en tales casos en los estándares de codificación C ++:

La principal desventaja de usar una excepción para informar un error de programación es que realmente no desea que se produzca el desbobinado de la pila cuando desea que el depurador se inicie en la línea exacta donde se detectó la violación, con el estado de la línea intacto. En resumen: hay errores que sabe que pueden ocurrir (consulte los Artículos 69 a 75). Para todo lo demás que no debería, y es culpa del programador si lo hace, lo hay assert.

Esa regla no está necesariamente establecida en piedra. En algunos casos más críticos, podría ser preferible usar, por ejemplo, envoltorios y un estándar de codificación que registre de manera uniforme dónde ocurren los errores del programador y throwen presencia de errores del programador, como tratar de deducir algo no válido o acceder a él fuera de los límites, porque Puede ser demasiado costoso no recuperarse en esos casos si el software tiene una oportunidad. Pero, en general, el uso más común del lenguaje tiende a favorecer no arrojar errores de programador.

Excepciones Externas

Donde veo las excepciones alentadas con mayor frecuencia en C ++ (según el comité estándar, por ejemplo) es para "excepciones externas", como en un resultado inesperado en alguna fuente externa fuera del programa. Un ejemplo es no asignar memoria. Otro es no abrir un archivo crítico requerido para que se ejecute el software. Otro falla al conectarse a un servidor requerido. Otro es un usuario que bloquea un botón de cancelación para cancelar una operación cuya ruta de ejecución de caso común espera tener éxito en ausencia de esta interrupción externa. Todas estas cosas están fuera del control del software inmediato y de los programadores que lo escribieron. Son resultados inesperados de fuentes externas que impiden que la operación (que realmente debería considerarse como una transacción indivisible en mi libro *) pueda tener éxito.

Actas

A menudo animo a ver un trybloque como una "transacción" porque las transacciones deberían tener éxito como un todo o fallar como un todo. Si estamos tratando de hacer algo y falla a la mitad, entonces cualquier efecto secundario / mutación realizado en el estado del programa generalmente debe revertirse para que el sistema vuelva a un estado válido como si la transacción nunca se hubiera ejecutado, así como un RDBMS que no puede procesar una consulta a la mitad no debe comprometer la integridad de la base de datos. Si muta el estado del programa directamente en dicha transacción, debe "activarlo" al encontrar un error (y aquí los protectores de alcance pueden ser útiles con RAII).

La alternativa mucho más simple es no mutar el estado original del programa; puede mutar una copia y luego, si tiene éxito, intercambie la copia con el original (asegurándose de que el intercambio no pueda arrojarse). Si falla, descarte la copia. Esto también se aplica incluso si no utiliza excepciones para el manejo de errores en general. Una mentalidad "transaccional" es clave para una recuperación adecuada si se han producido mutaciones en el estado del programa antes de encontrar un error. O tiene éxito como un todo o falla como un todo. A mitad de camino no logra hacer sus mutaciones.

Este es extrañamente uno de los temas menos discutidos cuando veo a los programadores preguntando cómo hacer un manejo adecuado de errores o excepciones, sin embargo, es el más difícil de todos acertar en cualquier software que quiera mutar directamente el estado del programa en muchos sus operaciones La pureza y la inmutabilidad pueden ayudar aquí a lograr una seguridad de excepción tanto como ayudan con la seguridad del hilo, ya que no es necesario revertir un efecto secundario externo / mutación que no ocurre.

Actuación

Otro factor rector en el uso o no de las excepciones es el rendimiento, y no me refiero de una manera obsesiva, centavo y contraproducente. Muchos compiladores de C ++ implementan lo que se llama "Manejo de excepciones de costo cero".

Ofrece una sobrecarga de tiempo de ejecución cero para una ejecución sin errores, que supera incluso la del manejo de errores de valor de retorno de C. Como compensación, la propagación de una excepción tiene una gran sobrecarga.

Según lo que he leído al respecto, hace que sus rutas de ejecución de casos comunes no requieran gastos generales (ni siquiera los gastos generales que normalmente acompañan el manejo y la propagación del código de error de estilo C), a cambio de sesgar los costos hacia las rutas excepcionales ( lo que significa que throwingahora es más caro que nunca).

"Caro" es un poco difícil de cuantificar, pero, para empezar, probablemente no quieras tirar un millón de veces en un circuito cerrado. Este tipo de diseño supone que las excepciones no ocurren de izquierda a derecha todo el tiempo.

No errores

Y ese punto de rendimiento me lleva a no errores, lo cual es sorprendentemente confuso si miramos todo tipo de otros idiomas. Pero yo diría, dado el diseño de EH de costo cero mencionado anteriormente, que casi seguramente no desea throwen respuesta a una clave que no se encuentra en un conjunto. Porque no solo es posiblemente un error (la persona que busca la clave podría haber creado el conjunto y esperar buscar claves que no siempre existen), sino que sería enormemente costoso en ese contexto.

Por ejemplo, una función de intersección de conjuntos puede querer recorrer dos conjuntos y buscar las claves que tienen en común. Si no puede encontrar una clave threw, estaría recorriendo y podría encontrar excepciones en la mitad o más de las iteraciones:

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

Ese ejemplo anterior es absolutamente ridículo y exagerado, pero he visto, en el código de producción, que algunas personas que vienen de otros lenguajes usan excepciones en C ++ algo así, y creo que es una declaración razonablemente práctica de que este no es un uso apropiado de excepciones. en C ++. Otra sugerencia anterior es que notará que el catchbloque no tiene absolutamente nada que hacer y está escrito para ignorar por la fuerza cualquier excepción, y eso generalmente es una sugerencia (aunque no es un garante) de que las excepciones probablemente no se usen de manera muy apropiada en C ++.

Para esos tipos de casos, algún tipo de valor de retorno que indica un error (cualquier cosa, desde volver falsea un iterador no válido nullptro lo que tenga sentido en el contexto) suele ser mucho más apropiado, y también a menudo más práctico y productivo ya que un tipo de error sin error el caso generalmente no requiere un proceso de desenrollado de pila para llegar al catchsitio analógico .

Preguntas

Tendría que ir con marcas de error interno si elijo evitar excepciones. ¿Será demasiado molesto de manejar o tal vez funcionará incluso mejor que las excepciones? Una comparación de ambos casos sería la mejor respuesta.

Evitar excepciones directamente en C ++ me parece extremadamente contraproducente, a menos que esté trabajando en algún sistema embebido o en un tipo particular de caso que prohíba su uso (en cuyo caso también tendría que hacer todo lo posible para evitar todo funcionalidad de biblioteca y lenguaje que de otro modo throw, como usar estrictamente nothrow new).

Si tiene que evitar excepciones por cualquier razón (por ejemplo, trabajando a través de los límites de la API de C de un módulo cuya API de C exporta), muchos podrían estar en desacuerdo conmigo, pero en realidad sugeriría usar un controlador / estado de error global como OpenGL con glGetError(). Puede hacer que use almacenamiento local de subprocesos para tener un estado de error único por subproceso.

Mi razón para ello es que no estoy acostumbrado a ver a los equipos en entornos de producción verificar minuciosamente todos los posibles errores, desafortunadamente, cuando se devuelven los códigos de error. Si fueran exhaustivas, algunas API de C pueden encontrar un error con casi todas las llamadas de API de C, y una verificación exhaustiva requeriría algo como:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

... con casi todas las líneas de código que invocan la API que requieren tales verificaciones Sin embargo, no he tenido la fortuna de trabajar con equipos tan minuciosos. A menudo ignoran tales errores la mitad, a veces incluso la mayoría de las veces. Ese es el mayor atractivo para mí de excepciones. Si ajustamos esta API y la hacemos de manera uniforme throwal encontrar un error, la excepción no puede ser ignorada , y en mi opinión y experiencia, ahí es donde radica la superioridad de las excepciones.

Pero si no se pueden usar excepciones, entonces el estado de error global por subproceso al menos tiene la ventaja (uno enorme en comparación con devolverme los códigos de error) de que podría tener la oportunidad de detectar un error anterior un poco más tarde que cuando ocurrió en una base de código descuidada en lugar de perderla por completo y dejarnos completamente ajenos a lo que sucedió. El error pudo haber ocurrido algunas líneas antes, o en una llamada de función anterior, pero siempre que el software no se haya bloqueado, podríamos comenzar a trabajar hacia atrás y descubrir dónde y por qué ocurrió.

Me parece que, dado que los punteros son raros, tendría que ir con indicadores de error internos si elijo evitar excepciones.

No diría necesariamente que los punteros son raros. Incluso hay métodos ahora en C ++ 11 y en adelante para obtener los punteros de datos subyacentes de los contenedores, y una nueva nullptrpalabra clave. En general, se considera imprudente usar punteros sin procesar para poseer / administrar memoria si puede usar algo como, en unique_ptrcambio, dada la importancia de cumplir con RAII en presencia de excepciones. Pero los punteros en bruto que no poseen / administran la memoria no se consideran necesariamente tan malos (incluso de personas como Sutter y Stroustrup) y, a veces, son muy prácticos como una forma de señalar las cosas (junto con los índices que apuntan a las cosas).

Podría decirse que no son menos seguros que los iteradores de contenedor estándar (al menos en versión, sin iteradores marcados) que no detectarán si intenta desreferenciarlos después de que se invaliden. C ++ sigue siendo, sin vergüenza, un lenguaje un poco peligroso, diría, a menos que su uso específico de él quiera envolver todo y ocultar incluso los punteros en bruto que no sean de su propiedad. Es casi crítico, con excepciones, que los recursos se ajusten a RAII (que generalmente no tiene costo de tiempo de ejecución), pero aparte de eso, no necesariamente trata de ser el lenguaje más seguro para evitar los costos que un desarrollador no desea explícitamente en intercambiar por otra cosa. El uso recomendado no es tratar de protegerlo de cosas como punteros colgantes e iteradores invalidados, por así decirlo (de lo contrario, se nos recomendaría usarshared_ptrpor todo el lugar, a lo que Stroustrup se opone vehementemente). Está tratando de protegerlo de no liberar / liberar / destruir / desbloquear / limpiar adecuadamente un recurso cuando algo throws.

Dragon Energy
fuente
14

Aquí está la cosa: debido a la historia y flexibilidad únicas de C ++, puede encontrar a alguien que proclame prácticamente cualquier opinión sobre cualquier característica que desee. Sin embargo, en general, cuanto más se parece a lo que estás haciendo, peor es la idea.

Una de las razones por las que C ++ es mucho más flexible cuando se trata de excepciones es que no puede exactamente return nilcuando lo desea. No existe nilen la gran mayoría de los casos y tipos.

Pero aquí está el hecho simple. Las excepciones hacen su trabajo automáticamente. Cuando lanzas una excepción, RAII se hace cargo y el compilador maneja todo. Cuando se utiliza códigos de error, usted tiene que recordar para comprobar ellos. Esto inherentemente hace que las excepciones sean significativamente más seguras que los códigos de error: se verifican a sí mismas, por así decirlo. Además, son más expresivos. Lanza una excepción y puedes obtener una cadena que te dice cuál es el error, e incluso puede contener información específica, como "Mal parámetro X que tenía el valor Y en lugar de Z". Obtenga un código de error de 0xDEADBEEF y, ¿qué salió exactamente? Espero que la documentación esté completa y actualizada, e incluso si obtiene "Parámetros incorrectos", nunca le dirá quéparámetro, qué valor tenía y qué valor debería haber tenido. Si capturas por referencia, también, pueden ser polimórficos. Finalmente, se pueden lanzar excepciones desde lugares donde los códigos de error nunca pueden, como los constructores. ¿Y qué hay de los algoritmos genéricos? ¿Cómo std::for_eachva a manejar su código de error? Pro-tip: no lo es.

Las excepciones son muy superiores a los códigos de error en todos los aspectos. La verdadera pregunta está en las excepciones frente a las afirmaciones.

Aquí está la cosa. Nadie puede saber de antemano qué condiciones previas tiene su programa para operar, qué condiciones de falla son inusuales, cuáles se pueden verificar de antemano y cuáles son errores. Esto generalmente significa que no puede decidir de antemano si una falla dada debería ser una afirmación o una excepción sin conocer la lógica del programa. Además, una operación que puede continuar cuando falla una de sus suboperaciones es la excepción , no la regla.

En mi opinión, hay excepciones para atrapar. No necesariamente de inmediato, sino eventualmente. Una excepción es un problema del que espera que el programa pueda recuperarse en algún momento. Pero la operación en cuestión nunca puede recuperarse de un problema que justifique una excepción.

Las fallas de afirmación son siempre errores fatales e irrecuperables. La corrupción de la memoria, ese tipo de cosas.

Entonces, cuando no puede abrir un archivo, ¿es una afirmación o una excepción? Bueno, en una biblioteca genérica, hay muchos escenarios en los que se puede manejar el error , por ejemplo, cargando un archivo de configuración, simplemente puede usar un valor predeterminado predefinido, por lo que diría que es una excepción.

Como nota al pie, me gustaría mencionar que hay algo relacionado con el "Patrón de objetos nulos". Este patrón es terrible . En diez años, será el próximo Singleton. El número de casos en los que puede producir un objeto nulo adecuado es minúsculo .

DeadMG
fuente
La alternativa no es necesariamente una int simple. Sería más probable que use algo similar a NSError al que estoy acostumbrado desde Obj-C.
Por Johansson
Está comparando no con C, sino con el Objetivo C. Es una gran diferencia. Y sus códigos de error son líneas explicativas. Todo lo que dices es correcto, solo que no responde a esta pregunta de alguna manera. Sin ofender.
Gangnus
Cualquiera que use Objective-C, por supuesto, le dirá que está absolutamente, completamente equivocado.
gnasher729
5

Las excepciones se inventaron por una razón, que es evitar que todo su código se vea así:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

En cambio, obtienes algo que se parece más a lo siguiente, con un controlador de excepción muy arriba maino de otra manera convenientemente ubicado:

result1 = function1();
result2 = function2();

Algunas personas reclaman beneficios de rendimiento para el enfoque sin excepción, pero en mi opinión, las preocupaciones de legibilidad superan las ganancias de rendimiento menores, especialmente cuando se incluye el tiempo de ejecución de toda la if (!success)plantilla que tiene que rociar en todas partes, o el riesgo de que sea más difícil de depurar segfaults si no lo hace ' t incluirlo, y teniendo en cuenta las posibilidades de que ocurran excepciones son relativamente raras.

No sé por qué Apple desalienta el uso de excepciones. Si intentan evitar la propagación de excepciones no controladas, todo lo que realmente logra es que las personas usen punteros nulos para indicar excepciones, por lo que un error del programador da como resultado una excepción de puntero nulo en lugar de una excepción de archivo no mucho más útil o lo que sea.

Karl Bielefeldt
fuente
1
Esto tendría más sentido si el código sin excepciones realmente se parece a eso, pero generalmente no lo es. Este patrón aparece cuando error_handler nunca regresa, pero rara vez lo contrario.
Por Johansson
1

En la publicación a la que hace referencia (excepciones vs códigos de error), creo que hay una discusión sutilmente diferente. La pregunta parece ser si tiene alguna lista global de códigos de error #define, probablemente completa con nombres como ERR001_FILE_NOT_WRITEABLE (o abreviado, si tiene mala suerte). Y, creo que el punto principal en ese hilo es que si estás programando en un lenguaje polifórfico, usando instancias de objetos, tal construcción de procedimiento no es necesaria. Las excepciones pueden expresar qué error ocurre simplemente en virtud de su tipo, y pueden encapsular información como qué mensaje imprimir (y otras cosas también). Entonces, tomaría esa conversación como una acerca de si debe programar de manera procesal en un lenguaje orientado a objetos o no.

Pero, la conversación sobre cuándo y cuánto confiar en las excepciones para manejar situaciones que surgen en el código es diferente. Cuando se lanzan y capturan excepciones, está introduciendo un paradigma de flujo de control completamente diferente de la pila de llamadas tradicional. El lanzamiento de excepciones es básicamente una declaración goto que lo saca de su pila de llamadas y lo envía a una ubicación indeterminada (donde su código de cliente decida manejar su excepción). Esto hace que la lógica de excepción sea muy difícil de razonar .

Y así, hay un sentimiento como el expresado por jheriko de que estos deberían minimizarse. Es decir, digamos que está leyendo un archivo que puede o no existir en el disco. Jheriko y Apple y aquellos que piensan como lo hacen (incluido yo mismo) argumentarían que lanzar una excepción no es apropiado; la ausencia de ese archivo no es excepcional , se espera. Las excepciones no deben sustituir el flujo de control normal. Es igual de fácil hacer que su método ReadFile () devuelva, por ejemplo, un valor booleano, y que el código del cliente vea, desde el valor de retorno falso, que no se encontró el archivo. El código del cliente puede decirle que no se encontró el archivo de usuario, o manejar silenciosamente ese caso, o lo que sea que quiera hacer.

Cuando lanzas una excepción, estás forzando una carga para tus clientes. Les está dando un código que, a veces, los sacará de la pila de llamadas normal y los obligará a escribir código adicional para evitar que su aplicación se bloquee. Una carga tan poderosa y discordante solo debería imponerse sobre ellos si es absolutamente necesario en tiempo de ejecución, o en interés de la filosofía de "fallar temprano". Entonces, en el primer caso, puede lanzar excepciones si el propósito de su aplicación es monitorear alguna operación de la red y alguien desconecta el cable de red (está señalando una falla catastrófica). En el último caso, puede lanzar una excepción si su método espera un parámetro no nulo y se le entrega nulo (está indicando que está siendo utilizado de manera inapropiada).

En cuanto a qué hacer en lugar de excepciones en el mundo orientado a objetos, tiene opciones más allá de la construcción del código de error de procedimiento. Uno que me viene a la mente de inmediato es que puedes crear objetos llamados OperationResult o algo así. Un método puede devolver un OperationResult y ese objeto puede tener información sobre si la operación tuvo éxito o no, y cualquier otra información que desee que tenga (un mensaje de estado, por ejemplo, pero también, más sutilmente, puede encapsular estrategias para la recuperación de errores ) Devolver esto en lugar de lanzar una excepción permite el polimorfismo, preserva el flujo de control y hace que la depuración sea mucho más simple.

Erik Dietrich
fuente
Estoy de acuerdo con usted, pero es la razón por la que la mayoría parece que no me hizo hacer la pregunta.
Por Johansson
0

Hay un par de capítulos encantadores en Clean Code que habla sobre este mismo tema, menos el punto de vista de Apple.

La idea básica es que nunca regresas nulo de tus funciones, porque:

  • Agrega mucho código duplicado
  • Hace un desastre de su código - Es decir: se hace más difícil de leer
  • A menudo puede dar lugar a errores de puntero nulo, o violaciones de acceso dependiendo de su "religión" de programación particular

La otra idea que lo acompaña es que use excepciones porque ayuda a reducir aún más el desorden en su código, y en lugar de tener que crear una serie de códigos de error que eventualmente se vuelven difíciles de rastrear, administrar y mantener (particularmente en múltiples módulos y plataformas) y son difíciles de descifrar para sus clientes; en su lugar, crea mensajes significativos que identifican la naturaleza exacta del problema, simplifican la depuración y pueden reducir su código de manejo de errores a algo tan simple como una try..catchdeclaración básica .

En mis días de desarrollo de COM, tenía un proyecto en el que cada falla generaba una excepción, y cada excepción necesitaba usar una identificación de código de error única basada en la extensión de las excepciones COM estándar de Windows. Fue un retraso de un proyecto anterior donde los códigos de error se habían utilizado previamente, pero la compañía quería orientarse a objetos y subirse al carro COM sin cambiar demasiado la forma en que hacían las cosas. Solo les tomó 4 meses quedarse sin números de código de error y se me ocurrió refactorizar grandes extensiones de código para usar excepciones. Es mejor ahorrarse el esfuerzo por adelantado y usar las excepciones con criterio.

S.Robins
fuente
Gracias, pero no estoy considerando devolver NULL. De todos modos, no parece posible frente a los constructores. Como mencioné, probablemente tendría que usar un indicador de error en el objeto.
Por Johansson