¿El uso de assert () en C ++ es una mala práctica?

92

Tiendo a agregar muchas afirmaciones a mi código C ++ para facilitar la depuración sin afectar el rendimiento de las versiones de lanzamiento. Ahora, assertes una macro C pura diseñada sin los mecanismos de C ++ en mente.

C ++, por otro lado std::logic_error, define , que debe lanzarse en los casos en que haya un error en la lógica del programa (de ahí el nombre). Lanzar una instancia podría ser la alternativa perfecta, más C ++ ish a assert.

El problema es que assert, y aborttanto por terminado el programa inmediatamente sin llamar a los destructores, por lo tanto, sin esperar la limpieza, mientras que lanzar una excepción de tiempo de ejecución añade manualmente los costos innecesarios. Una forma de evitar esto sería crear una macro de aserción propia SAFE_ASSERT, que funciona como la contraparte de C, pero arroja una excepción en caso de falla.

Puedo pensar en tres opiniones sobre este problema:

  • Cíñete a la afirmación de C. Dado que el programa se termina inmediatamente, no importa si los cambios se han desenrollado correctamente. Además, usar #defines en C ++ es igual de malo.
  • Lanza una excepción y capturala en main () . Permitir que el código omita los destructores en cualquier estado del programa es una mala práctica y debe evitarse a toda costa, al igual que las llamadas a terminate (). Si se lanzan excepciones, deben detectarse.
  • Lanza una excepción y deja que termine el programa. Una excepción que finaliza un programa está bien y, debido a NDEBUGesto, esto nunca sucederá en una versión de lanzamiento. La captura es innecesaria y expone los detalles de implementación del código interno a main().

¿Existe una respuesta definitiva a este problema? ¿Alguna referencia profesional?

Editado: Saltar destructores no es, por supuesto, un comportamiento indefinido.

Fabián Knorr
fuente
22
No, de verdad, logic_errores el error lógico. Un error en la lógica del programa se llama error. No resuelve errores lanzando excepciones.
R. Martinho Fernandes
4
Afirmaciones, excepciones, códigos de error. Cada uno tiene un caso de uso completamente distinto, y no debe usar uno donde se necesite otro.
Kerrek SB
5
Asegúrese de usarlo static_assertdonde sea apropiado si lo tiene disponible.
Flexo
4
@trion No veo cómo eso ayuda. ¿Lanzarías std::bug?
R. Martinho Fernandes
3
@trion: No hagas eso. Las excepciones no son para depuración. Alguien podría estar detectando la excepción. No hay necesidad de preocuparse por UB al llamar std::abort(); simplemente generará una señal que hará que el proceso termine.
Kerrek SB

Respuestas:

73

Las afirmaciones son completamente apropiadas en código C ++. Las excepciones y otros mecanismos de manejo de errores no están pensados ​​para lo mismo que las afirmaciones.

El manejo de errores es para cuando existe la posibilidad de recuperar o informar un error al usuario. Por ejemplo, si hay un error al intentar leer un archivo de entrada, es posible que desee hacer algo al respecto. Los errores pueden deberse a errores, pero también pueden ser simplemente la salida adecuada para una entrada determinada.

Las afirmaciones son para cosas como verificar que se cumplan los requisitos de una API cuando la API normalmente no se verificaría, o para verificar cosas que el desarrollador cree que está garantizado por construcción. Por ejemplo, si un algoritmo requiere una entrada ordenada, normalmente no lo comprobaría, pero es posible que tenga una afirmación para comprobarlo para que las compilaciones de depuración marquen ese tipo de error. Una afirmación siempre debe indicar un programa que funciona incorrectamente.


Si está escribiendo un programa en el que un apagado incorrecto podría causar un problema, es posible que desee evitar las afirmaciones. El comportamiento indefinido estrictamente en términos del lenguaje C ++ no califica como tal problema aquí, ya que hacer una afirmación probablemente ya sea el resultado de un comportamiento indefinido o la violación de algún otro requisito que podría evitar que una limpieza funcione correctamente.

Además, si implementa aserciones en términos de una excepción, potencialmente podría ser detectado y 'manejado' aunque esto contradiga el propósito mismo de la aserción.

bames53
fuente
1
No estoy del todo seguro de si esto se indicó específicamente en la respuesta, así que lo declararé aquí: no debe usar una aserción para nada que involucre la entrada del usuario que no se pueda determinar al momento de escribir el código. Si un usuario pasa en 3lugar de 1a su código, en general, no debería desencadenar una aserción. Las afirmaciones son solo errores del programador, no del usuario de la biblioteca o error de la aplicación.
SS Anne
101
  • Las afirmaciones son para depurar . El usuario de su código enviado nunca debería verlos. Si se acierta una afirmación, su código debe corregirse.

    CWE-617: Afirmación accesible

El producto contiene una afirmación () o una declaración similar que puede ser activada por un atacante, lo que conduce a la salida de una aplicación u otro comportamiento más grave de lo necesario.

Si bien la aserción es buena para detectar errores lógicos y reducir las posibilidades de alcanzar condiciones de vulnerabilidad más graves, aún puede conducir a una denegación de servicio.

Por ejemplo, si un servidor maneja múltiples conexiones simultáneas y se produce una aserción () en una sola conexión que hace que se caigan todas las demás conexiones, esta es una aserción alcanzable que conduce a una denegación de servicio.

  • Las excepciones son por circunstancias excepcionales . Si se encuentra uno, el usuario no podrá hacer lo que quiere, pero puede continuar en otro lugar.

  • El manejo de errores es para el flujo normal del programa. Por ejemplo, si le pide al usuario un número y obtiene algo que no se puede analizar, eso es normal , porque la entrada del usuario no está bajo su control y siempre debe manejar todas las situaciones posibles de forma natural. (Por ejemplo, repita hasta que tenga una entrada válida, diciendo "Lo siento, inténtelo de nuevo" en el medio).

Kerrek SB
fuente
1
vino buscando esta reafirmación; cualquier forma de aserción que pase al código de producción indica un diseño y control de calidad deficientes. El punto donde se llama a una aserción es donde debería ser el manejo elegante de una condición de error. (Yo nunca uso el de assert). En cuanto a las excepciones, el único caso de uso que conozco es cuando ctor puede fallar, todos los demás son para el manejo normal de errores.
slashmais
5
@slashmais: El sentimiento es loable, pero a menos que esté enviando un código perfecto y sin errores, considero que una afirmación (incluso una que bloquee al usuario) es preferible a un comportamiento indefinido. Los errores ocurren en sistemas complejos, y con una afirmación tienes una forma de verlos y diagnosticarlos donde ocurren.
Kerrek SB
@KerrekSB Preferiría usar una excepción sobre una aserción. Al menos, el código tiene la posibilidad de descartar la rama que falla y hacer algo más útil. Como mínimo, si está utilizando RAII, todos los búferes para abrir archivos se descargarán correctamente.
daemonspring
14

Las afirmaciones se pueden usar para verificar invariantes de implementación interna, como el estado interno antes o después de la ejecución de algún método, etc. Si la afirmación falla, significa que la lógica del programa está rota y no puede recuperarse de esto. En este caso, lo mejor que puede hacer es romper lo antes posible sin pasar una excepción al usuario. Lo realmente bueno de las afirmaciones (al menos en Linux) es que el volcado del núcleo se genera como resultado de la terminación del proceso y, por lo tanto, puede investigar fácilmente el seguimiento de la pila y las variables. Esto es mucho más útil para comprender la falla lógica que el mensaje de excepción.

Nogard
fuente
Tengo un enfoque similar. Utilizo afirmaciones para la lógica que probablemente deberían ser correctas localmente (por ejemplo, invariantes de bucle). Las excepciones son para aquellos casos en los que un error lógico fue impuesto al código por una situación no local (externa).
Spraff
Si una afirmación falla, significa que la lógica de parte del programa está rota. Una afirmación fallida no implica necesariamente que no se pueda lograr nada . Un complemento roto probablemente no debería abortar un procesador de texto completo.
daemonspring
13

¡No ejecutar destructores debido a abortar () no es un comportamiento indefinido!

Si lo fuera, también sería un comportamiento indefinido llamar std::terminate(), y entonces, ¿cuál sería el punto de proporcionarlo?

assert() es tan útil en C ++ como en C. Las afirmaciones no son para el manejo de errores, son para abortar el programa inmediatamente.

Jonathan Wakely
fuente
1
Yo diría que abort()es para abortar el programa de inmediato. Sin embargo, tiene razón en que las aserciones no son para el manejo de errores, sin embargo, assert intenta manejar el error abortando. ¿No debería lanzar una excepción y dejar que la persona que llama maneje el error si puede? Después de todo, la persona que llama está en una mejor posición para determinar si la falla de una función hace que no valga la pena hacer otra cosa. Tal vez la persona que llama está tratando de hacer tres cosas no relacionadas y aún podría completar los otros dos trabajos y simplemente descartar este.
daemonspring
Y assertestá definido para llamar abort(cuando la condición es falsa). En cuanto a lanzar excepciones, no, eso no siempre es apropiado. Algunas cosas no pueden ser manejadas por la persona que llama. La persona que llama no puede determinar si un error lógico en una función de biblioteca de terceros es recuperable o si se pueden corregir datos corruptos.
Jonathan Wakely
6

En mi humilde opinión, las afirmaciones son para verificar condiciones que, si se violan, hacen que todo lo demás sea una tontería. Y por tanto no puedes recuperarte de ellos o más bien, recuperarse es irrelevante.

Los agruparía en 2 categorías:

  • El desarrollador peca (por ejemplo, una función de probabilidad que devuelve valores negativos):

probabilidad flotante () {retorno -1.0; }

afirmar (probabilidad ()> = 0.0)

  • La máquina está rota (por ejemplo, la máquina que ejecuta su programa está muy mal):

int x = 1;

afirmar (x> 0);

Ambos son ejemplos triviales pero no demasiado alejados de la realidad. Por ejemplo, piense en algoritmos ingenuos que devuelven índices negativos para usar con vectores. O programas integrados en hardware personalizado. O más bien porque sucede una mierda .

Y si hay tales errores de desarrollo, no debe confiar en ningún mecanismo de recuperación o manejo de errores implementado. Lo mismo se aplica a los errores de hardware.

FranMowinckel
fuente
1
afirmar (probabilidad ()> = 0.0)
Elliott