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 throw
en 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 try
bloque 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 throwing
ahora 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 throw
en 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 catch
bloque 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 false
a un iterador no válido nullptr
o 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 catch
sitio 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 throw
al 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 nullptr
palabra clave. En general, se considera imprudente usar punteros sin procesar para poseer / administrar memoria si puede usar algo como, en unique_ptr
cambio, 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_ptr
por 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
.