Soy un gran admirador de escribir assert
cheques en el código C ++ como una forma de detectar casos durante el desarrollo que posiblemente no sucedan pero sucedan debido a errores lógicos en mi programa. Esta es una buena práctica en general.
Sin embargo, he notado que algunas funciones que escribo (que son parte de una clase compleja) tienen más de 5 afirmaciones, lo que parece que podría ser una mala práctica de programación, en términos de legibilidad y facilidad de mantenimiento. Creo que sigue siendo genial, ya que cada uno requiere que piense en las condiciones previas y posteriores de las funciones y realmente ayudan a detectar errores. Sin embargo, solo quería publicar esto para preguntar si hay mejores paradigmas para detectar errores lógicos en los casos en que sea necesario un gran número de comprobaciones.
Comentario de Emacs : dado que Emacs es mi IDE de elección, tengo un poco gris las declaraciones de afirmación que ayudan a reducir la sensación de desorden que pueden proporcionar. Esto es lo que agrego a mi archivo .emacs:
; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))
; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))
fuente
Respuestas:
He visto cientos de errores que se habrían solucionado más rápido si alguien hubiera escrito más afirmaciones, y ninguno que hubiera sido resuelto más rápido escribiendo menos .
La legibilidad podría ser un problema, tal vez, aunque según mi experiencia, las personas que escriben buenas afirmaciones también escriben código legible. Y nunca me molesta ver que el comienzo de una función comienza con un bloque de afirmaciones para verificar que los argumentos no sean basura, simplemente coloque una línea en blanco después.
También en mi experiencia, la capacidad de mantenimiento siempre se mejora mediante afirmaciones, al igual que con las pruebas unitarias. Las afirmaciones proporcionan una comprobación de la cordura de que el código se está utilizando de la manera en que se pretendía utilizar.
fuente
Bueno, por supuesto que lo es. [Imagine un ejemplo desagradable aquí.] Sin embargo, aplicando las pautas detalladas a continuación, no debería tener problemas para superar ese límite en la práctica. También soy un gran admirador de las afirmaciones, y las uso de acuerdo con estos principios. Gran parte de este consejo no es especial para las afirmaciones, sino que solo se les aplica una buena práctica general de ingeniería.
Tenga en cuenta la sobrecarga de tiempo de ejecución y huella binaria en mente
Las afirmaciones son geniales, pero si hacen que su programa sea inaceptablemente lento, será muy molesto o los apagará tarde o temprano.
Me gusta medir el costo de una afirmación en relación con el costo de la función que contiene. Considere los siguientes dos ejemplos.
La función en sí es una operación O (1), pero las aserciones representan la sobrecarga de O ( n ). No creo que le gustaría que tales controles estén activos a menos que en circunstancias muy especiales.
Aquí hay otra función con afirmaciones similares.
La función en sí es una operación O ( n ), por lo que duele mucho menos agregar una sobrecarga adicional O ( n ) para la aserción. Disminuir la velocidad de una función mediante un factor constante pequeño (en este caso, probablemente menos de 3) es algo que generalmente podemos permitirnos en una compilación de depuración, pero tal vez no en una compilación de lanzamiento.
Ahora considere este ejemplo.
Si bien muchas personas probablemente se sentirán mucho más cómodas con esta afirmación O (1) que con las dos afirmaciones O ( n ) en el ejemplo anterior, en mi opinión son moralmente equivalentes. Cada uno agrega una sobrecarga en el orden de la complejidad de la función misma.
Finalmente, están las afirmaciones "realmente baratas" que están dominadas por la complejidad de la función en la que están contenidas.
Aquí, tenemos dos aserciones O (1) en una función O ( n ). Probablemente no será un problema mantener esta sobrecarga incluso en las versiones de lanzamiento.
Sin embargo, tenga en cuenta que las complejidades asintóticas no siempre dan una estimación adecuada porque, en la práctica, siempre estamos tratando con tamaños de entrada limitados por algunos factores finitos constantes y constantes ocultos por "Big- O " que muy bien podrían no ser insignificantes.
Entonces, ahora que hemos identificado diferentes escenarios, ¿qué podemos hacer al respecto? Un enfoque (probablemente demasiado) fácil sería seguir una regla como “No use afirmaciones que dominen la función en la que están contenidas”. Si bien podría funcionar para algunos proyectos, otros podrían necesitar un enfoque más diferenciado. Esto podría hacerse utilizando diferentes macros de aserción para los diferentes casos.
Ahora puede usar las tres macros y
MY_ASSERT_LOW
, en lugar de la macro estándar de "un tamaño para todos" de la biblioteca estándar, para aserciones que están dominadas por, ni dominadas por, ni dominando y dominando la complejidad de su función de contención respectivamente. Cuando compila el software, puede predefinir el símbolo del preprocesador para seleccionar qué tipo de aserciones deberían convertirse en el ejecutable. Las constantes y no corresponden a ninguna macros de aserción y están destinadas a usarse como valores para activar o desactivar todas las aserciones, respectivamente.MY_ASSERT_MEDIUM
MY_ASSERT_HIGH
assert
MY_ASSERT_COST_LIMIT
MY_ASSERT_COST_NONE
MY_ASSERT_COST_ALL
MY_ASSERT_COST_LIMIT
Estamos confiando en el supuesto aquí de que un buen compilador no generará ningún código para
y transformar
dentro
lo cual creo que es una suposición segura hoy en día.
Si está a punto de modificar el código anterior, considere las anotaciones específicas del compilador como
__attribute__ ((cold))
onmy::assertion_failed
o__builtin_expect(…, false)
on!(CONDITION)
para reducir la sobrecarga de las afirmaciones aprobadas. En las versiones de lanzamiento, también puede considerar reemplazar la llamada a la funciónmy::assertion_failed
con algo como__builtin_trap
reducir la huella por el inconveniente de perder un mensaje de diagnóstico.Este tipo de optimizaciones solo son relevantes en aserciones extremadamente baratas (como comparar dos enteros que ya se dan como argumentos) en una función que es muy compacta, sin tener en cuenta el tamaño adicional del binario acumulado al incorporar todas las cadenas de mensajes.
Compara cómo este código
se compila en el siguiente ensamblado
mientras que el siguiente código
da esta asamblea
con el que me siento mucho más cómodo. (Ejemplos se ensayaron con GCC 5.3.0 usando el
-std=c++14
,-O3
y-march=native
banderas en 4.3.3-2-ARCH x86_64 GNU / Linux. No se muestra en los fragmentos anteriores son las declaraciones detest::positive_difference_1st
ytest::positive_difference_2nd
que he añadido el__attribute__ ((hot))
al.my::assertion_failed
Fue declarado con__attribute__ ((cold))
.)Afirmar condiciones previas en la función que depende de ellas
Suponga que tiene la siguiente función con el contrato especificado.
En lugar de escribir
en cada sitio de llamada, ponga esa lógica una vez en la definición de
count_letters
y llámalo sin más preámbulos.
Esto tiene las siguientes ventajas.
assert
declaraciones en su código.La desventaja obvia es que no obtendrá la ubicación de origen del sitio de llamada en el mensaje de diagnóstico. Creo que este es un problema menor. Un buen depurador debería permitirle rastrear el origen de la violación del contrato convenientemente.
El mismo pensamiento se aplica a las funciones "especiales" como los operadores sobrecargados. Cuando escribo iteradores, generalmente, si la naturaleza del iterador lo permite, les doy una función miembro
eso permite preguntar si es seguro desreferenciar el iterador. (Por supuesto, en la práctica, casi siempre solo es posible garantizar que no será seguro desreferenciar el iterador. Pero creo que aún puede detectar muchos errores con dicha función). En lugar de ensuciar todo mi código que usa el iterador con
assert(iter.good())
declaraciones, prefiero poner un soloassert(this->good())
como la primera línea de laoperator*
implementación del iterador.Si está utilizando la biblioteca estándar, en lugar de afirmar manualmente sus precondiciones en su código fuente, active sus comprobaciones en las compilaciones de depuración. Pueden hacer comprobaciones aún más sofisticadas, como probar si el contenedor al que se refiere un iterador todavía existe. (Consulte la documentación de libstdc ++ y libc ++ (trabajo en progreso) para obtener más información).
Factorizar condiciones comunes fuera
Supongamos que está escribiendo un paquete de álgebra lineal. Muchas funciones tendrán precondiciones complicadas y violarlas a menudo causará resultados incorrectos que no son inmediatamente reconocibles como tales. Sería muy bueno si estas funciones afirmaran sus condiciones previas. Si define un grupo de predicados que le dicen ciertas propiedades sobre una estructura, esas afirmaciones se vuelven mucho más legibles.
También le dará más mensajes de error útiles.
ayuda mucho más que, digamos
donde primero tendrías que ir a mirar el código fuente en el contexto para descubrir lo que realmente se probó.
Si tiene una
class
con invariantes no triviales, es probable que sea una buena idea afirmarlos de vez en cuando cuando se haya metido con el estado interno y quiera asegurarse de que está dejando el objeto en un estado válido al regresar.Para este propósito, me pareció útil definir una
private
función miembro a la que llamo convencionalmenteclass_invaraiants_hold_
. Suponga que está volviendo a implementarstd::vector
(porque todos sabemos que no es lo suficientemente bueno), podría tener una función como esta.Observe algunas cosas sobre esto.
const
ynoexcept
, de acuerdo con la directriz, las afirmaciones no tendrán efectos secundarios. Si tiene sentido, también lo declaraconstexpr
.assert(this->class_invariants_hold_())
. De esta manera, si las aserciones se compilan, podemos estar seguros de que no se incurre en gastos generales de tiempo de ejecución.if
declaraciones conreturn
s temprana en lugar de una gran expresión. Esto facilita el paso a través de la función en un depurador y descubrir qué parte de la invariante se rompió si se dispara la aserción.No afirmes sobre cosas tontas
Algunas cosas simplemente no tienen sentido para afirmar.
Estas afirmaciones no hacen que el código sea aún más legible o más fácil de razonar. Todos los programadores de C ++ deben confiar lo suficiente en cómo
std::vector
funciona para asegurarse de que el código anterior sea correcto simplemente al mirarlo. No estoy diciendo que nunca debas afirmar el tamaño de un contenedor. Si ha agregado o eliminado elementos utilizando un flujo de control no trivial, tal afirmación puede ser útil. Pero si simplemente repite lo que se escribió en el código de no aserción justo arriba, no se gana valor.Tampoco afirme que las funciones de la biblioteca funcionan correctamente.
Si confías en la biblioteca tan poco, mejor considera usar otra biblioteca en su lugar.
Por otro lado, si la documentación de la biblioteca no es 100% clara y usted gana confianza sobre sus contratos leyendo el código fuente, tiene mucho sentido afirmar sobre ese "contrato inferido". Si está roto en una versión futura de la biblioteca, lo notará rápidamente.
Esto es mejor que la siguiente solución que no le dirá si sus suposiciones eran correctas.
No abuse de las afirmaciones para implementar la lógica del programa
Las afirmaciones solo deben usarse para descubrir errores que merezcan matar inmediatamente su aplicación. No deben usarse para verificar ninguna otra condición, incluso si la reacción apropiada a esa condición también fuera dejar de fumar inmediatamente.
Por lo tanto, escribe esto ...
…en lugar de eso.
Además, nunca utilizar afirmaciones para validar la entrada de confianza o se compruebe que
std::malloc
no lo hicieronreturn
que elnullptr
. Incluso si sabe que nunca desactivará las afirmaciones, incluso en las versiones de lanzamiento, una afirmación le comunica al lector que verifica algo que siempre es cierto dado que el programa está libre de errores y no tiene efectos secundarios visibles. Si este no es el tipo de mensaje que desea comunicar, utilice un mecanismo alternativo de manejo de errores, comothrow
una excepción. Si le parece conveniente tener un contenedor macro para sus comprobaciones de no aserción, continúe escribiendo uno. Simplemente no lo llame "afirmar", "asumir", "exigir", "garantizar" o algo así. Su lógica interna podría ser la misma que paraassert
, excepto que nunca se compila, por supuesto.Más información
He encontrado charla John Lakos' programación defensiva bien hecha , dada en CppCon'14 ( 1 st parte , 2 ª parte ) muy ilustrativo. Él toma la idea de personalizar qué afirmaciones están habilitadas y cómo reaccionar ante excepciones fallidas aún más de lo que hice en esta respuesta.
fuente
Assertions are great, but ... you will turn them off sooner or later.
- Ojalá antes, como antes de que se envíe el código. Las cosas que necesitan hacer que el programa muera en la producción deberían ser parte del código "real", no en afirmaciones.Me parece que con el tiempo escribo menos afirmaciones porque muchas de ellas equivalen a "está funcionando el compilador" y "está funcionando la biblioteca". Una vez que empieces a pensar en qué estás probando exactamente, sospecho que escribirás menos afirmaciones.
Por ejemplo, un método que (digamos) agrega algo a una colección no debería tener que afirmar que la colección existe, que generalmente es una condición previa de la clase que posee el mensaje o es un error fatal que debería devolverlo al usuario . Así que verifíquelo una vez, muy pronto, luego asuma.
Las afirmaciones para mí son una herramienta de depuración, y generalmente las usaré de dos maneras: encontrando un error en mi escritorio (y no se registran. Bueno, tal vez la clave sea una); y encontrar un error en el escritorio del cliente (y se registran). Ambas veces estoy usando aserciones principalmente para generar un seguimiento de la pila después de forzar una excepción lo antes posible. Tenga en cuenta que las afirmaciones utilizadas de esta manera pueden conducir fácilmente a errores de seguridad : es posible que el error nunca ocurra en la compilación de depuración que tiene habilitadas las aserciones.
fuente
Muy pocas afirmaciones: buena suerte cambiando ese código plagado de suposiciones ocultas.
Demasiadas afirmaciones: pueden conducir a problemas de legibilidad y potencialmente a olores de código: ¿la clase, la función y la API están diseñadas correctamente cuando tiene tantos supuestos colocados en las declaraciones de afirmación?
También podría haber afirmaciones que realmente no comprueban nada o comprueban cosas como la configuración del compilador en cada función: /
Apunte al punto óptimo, pero no menos (como alguien más ya dijo, "más" de las afirmaciones es menos dañino que tener muy pocas o que Dios nos ayude, ninguna).
fuente
Sería increíble si pudiera escribir una función Assert que tomara solo una referencia a un método CONST booleano, de esta manera está seguro de que sus afirmaciones no tienen efectos secundarios al garantizar que se use un método const booleano para probar la afirmación
sacaría un poco de legibilidad, especialmente porque no creo que pueda anotar una lambda (en c ++ 0x) para que sea una constante para alguna clase, lo que significa que no puede usar lambdas para eso
demasiado si me preguntas, pero si comenzara a ver un cierto nivel de contaminación debido a afirmaciones, desconfiaría de dos cosas:
fuente
He escrito en C # mucho más que en C ++, pero los dos lenguajes no están muy separados. En .Net uso Asserts para condiciones que no deberían suceder, pero a menudo también lanzo excepciones cuando no hay forma de continuar. El depurador VS2010 me muestra mucha información buena sobre una excepción, sin importar cuán optimizada sea la versión de lanzamiento. También es una buena idea agregar pruebas unitarias si puede. A veces, el registro también es bueno tener como ayuda para la depuración.
Entonces, ¿puede haber demasiadas afirmaciones? Sí. Elegir entre Abortar / Ignorar / Continuar 15 veces en un minuto se vuelve molesto. Se lanza una excepción solo una vez. Es difícil cuantificar el punto en el que hay demasiadas afirmaciones, pero si sus aserciones cumplen el papel de aserciones, excepciones, pruebas unitarias y registro, entonces algo está mal.
Me gustaría reservar aserciones para los escenarios que no deberían suceder. Puede sobreafirmar inicialmente, porque las aserciones son más rápidas de escribir, pero vuelva a factorizar el código más tarde; convierta algunas de ellas en excepciones, otras en pruebas, etc. Si tiene suficiente disciplina para limpiar cada comentario de TODO, deje un comente al lado de cada uno que planea volver a trabajar y NO OLVIDE abordar el TODO más adelante.
fuente
¡Quiero trabajar contigo! Alguien que escribe mucho
asserts
es fantástico. No sé si hay tal cosa como "demasiados". Mucho más común para mí son las personas que escriben muy poco y, en última instancia, terminan tropezando con el ocasional problema mortal de UB que solo aparece en luna llena, que podría haberse reproducido fácilmente repetidamente con un simpleassert
.Mensaje de falla
Lo único en lo que puedo pensar es incrustar información de fallas en el
assert
si aún no lo está haciendo, así:De esta manera, es posible que ya no sientas que tienes demasiados si aún no lo estabas haciendo, ya que ahora estás haciendo que tus afirmaciones desempeñen un papel más importante en la documentación de supuestos y condiciones previas.
Efectos secundarios
Por supuesto,
assert
puede ser mal utilizado e introducir errores, así:... Si
foo()
desencadena efectos secundarios, debe tener mucho cuidado con eso, pero estoy seguro de que ya es uno de los que afirma con mucha libertad (un "afirmador experimentado"). Esperemos que su procedimiento de prueba también sea tan bueno como su cuidadosa atención para afirmar suposiciones.Velocidad de depuración
Si bien la velocidad de la depuración generalmente debe estar en la parte inferior de nuestra lista de prioridades, una vez terminé afirmando tanto en una base de código antes de que la ejecución de la compilación de depuración a través del depurador fuera más de 100 veces más lenta que el lanzamiento.
Fue principalmente porque tenía funciones como esta:
... donde cada llamada a
operator[]
haría una aserción de verificación de límites. Terminé reemplazando algunos de esos críticos para el rendimiento con equivalentes inseguros que no afirman solo acelerar la construcción de depuración drásticamente a un costo menor para solo la seguridad de nivel de detalle de implementación, y solo porque el golpe de velocidad estaba comenzando para degradar notablemente la productividad (hacer que el beneficio de una depuración más rápida supere el costo de perder algunas afirmaciones, pero solo para funciones como esta función de producto cruzado que se estaba utilizando en las rutas más críticas y medidas, nooperator[]
en general).Principio de responsabilidad única
Si bien no creo que realmente puedas equivocarte con más afirmaciones (al menos es mucho, mucho mejor errar por el lado de demasiadas que muy pocas), las afirmaciones en sí mismas pueden no ser un problema, pero pueden estar indicando uno.
Si tiene como 5 aserciones a una sola llamada de función, por ejemplo, podría estar haciendo demasiado. Su interfaz puede tener demasiadas condiciones previas y parámetros de entrada, por ejemplo, considero que no está relacionado solo con el tema de lo que constituye un número saludable de afirmaciones (por lo que generalmente respondería, "¡mientras más, mejor!"), Pero eso podría ser una posible bandera roja (o muy posiblemente no).
fuente
Es muy razonable agregar cheques a su código. Para una afirmación simple (la que está integrada en el compilador de C y C ++), mi patrón de uso es que una afirmación fallida significa que hay un error en el código que debe corregirse. Interpreto esto un poco generosamente; si espero una petición web para devolver un estado 200 y afirmar por ello sin gastos de envío otros casos a continuación, una aserción fallida no muestran ciertamente un error en mi código, por lo que la aserción está justificada.
Entonces, cuando la gente dice que una afirmación de que solo verifica lo que hace el código es superflua, eso no está del todo bien. Esa afirmación verifica lo que creen que hace el código, y el objetivo de la afirmación es verificar que la suposición de que no hay errores en el código es correcta. Y la afirmación también puede servir como documentación. Si supongo que después de ejecutar un bucle i == ny no es 100% obvio por el código, entonces "afirmar (i == n)" será útil.
Es mejor tener más que simplemente "afirmar" en su repertorio para manejar diferentes situaciones. Por ejemplo, la situación en la que verifico que algo no sucede que indicaría un error, pero aún así sigo trabajando alrededor de esa condición. (Por ejemplo, si uso algo de caché, entonces podría verificar si hay errores, y si un error ocurre inesperadamente, puede ser seguro arreglar el error tirando el caché. Quiero algo que sea casi una afirmación, que me dice durante el desarrollo , y todavía me deja continuar.
Otro ejemplo es la situación en la que no espero que ocurra algo, tengo una solución genérica, pero si esto sucede, quiero saberlo y examinarlo. De nuevo, algo casi como una afirmación, que debería decirme durante el desarrollo. Pero no del todo una afirmación.
Demasiadas afirmaciones: si una afirmación bloquea su programa cuando está en manos del usuario, entonces no debe tener ninguna afirmación que se bloquee debido a falsos negativos.
fuente
Depende. Si los requisitos del código están claramente documentados, la afirmación siempre debe coincidir con los requisitos. En cuyo caso es algo bueno. Sin embargo, si no hay requisitos o requisitos mal escritos, sería difícil para los nuevos programadores editar el código sin tener que consultar la prueba de la unidad cada vez para averiguar cuáles son los requisitos.
fuente