¿Es posible escribir demasiadas afirmaciones?

33

Soy un gran admirador de escribir assertcheques 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)))))
Alan Turing
fuente
3
Debo admitir que esta es una pregunta que se me ha pasado por la cabeza de vez en cuando. Interesado en escuchar las opiniones de otros sobre esto.
Captain Sensible

Respuestas:

45

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 .

[C] ¿[demasiadas afirmaciones] podría ser una mala práctica de programación, en términos de legibilidad y facilidad de mantenimiento [?]

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.

Bob Murphy
fuente
1
Buena respuesta. También agregué una descripción a la pregunta de cómo mejoro la legibilidad con Emacs.
Alan Turing
2
"Ha sido mi experiencia que las personas que escriben buenas afirmaciones también escriben código legible" << punto excelente. Hacer que el código sea legible depende tanto del programador individual como de las técnicas que él o ella es y no está permitido usar. He visto que las buenas técnicas se vuelven ilegibles en las manos equivocadas, e incluso lo que la mayoría consideraría malas técnicas se vuelve perfectamente claro, incluso elegante, mediante el uso adecuado de la abstracción y los comentarios.
Greg Jackson
He tenido algunos bloqueos de aplicaciones que fueron causados ​​por afirmaciones erróneas. Así que he visto errores que no hubieran existido si alguien (yo) hubiera escrito menos afirmaciones.
CodesInChaos
@CodesInChaos Podría decirse que, aparte de los errores tipográficos, esto apunta a un error en la formulación del problema, es decir, el error estaba en el diseño, de ahí la falta de coincidencia entre las afirmaciones y el código (otro).
Lawrence
12

¿Es posible escribir demasiadas afirmaciones?

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.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

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.

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

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.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

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.

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

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.

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

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_MEDIUMMY_ASSERT_HIGHassertMY_ASSERT_COST_LIMITMY_ASSERT_COST_NONEMY_ASSERT_COST_ALLMY_ASSERT_COST_LIMIT

Estamos confiando en el supuesto aquí de que un buen compilador no generará ningún código para

if (false_constant_expression && run_time_expression) { /* ... */ }

y transformar

if (true_constant_expression && run_time_expression) { /* ... */ }

dentro

if (run_time_expression) { /* ... */ }

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))on my::assertion_failedo __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ón my::assertion_failedcon algo como __builtin_trapreducir 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

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

se compila en el siguiente ensamblado

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

mientras que el siguiente código

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

da esta asamblea

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

con el que me siento mucho más cómodo. (Ejemplos se ensayaron con GCC 5.3.0 usando el -std=c++14, -O3y -march=nativebanderas en 4.3.3-2-ARCH x86_64 GNU / Linux. No se muestra en los fragmentos anteriores son las declaraciones de test::positive_difference_1sty test::positive_difference_2ndque he añadido el __attribute__ ((hot))al. my::assertion_failedFue 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.

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

En lugar de escribir

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

en cada sitio de llamada, ponga esa lógica una vez en la definición de count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

y llámalo sin más preámbulos.

const auto frequency = count_letters(text, letter);

Esto tiene las siguientes ventajas.

  • Solo necesita escribir el código de aserción una vez. Dado que el propósito de las funciones es que se llamen, a menudo más de una vez, esto debería reducir el número total de assertdeclaraciones en su código.
  • Mantiene la lógica que verifica las condiciones previas cerca de la lógica que depende de ellas. Creo que este es el aspecto más importante. Si sus clientes hacen un mal uso de su interfaz, tampoco se puede suponer que apliquen las afirmaciones correctamente, por lo que es mejor que la función les diga.

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

bool
good() const noexcept;

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 solo assert(this->good())como la primera línea de la operator*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.

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

También le dará más mensajes de error útiles.

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

ayuda mucho más que, digamos

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

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 classcon 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 privatefunción miembro a la que llamo convencionalmente class_invaraiants_hold_. Suponga que está volviendo a implementar std::vector(porque todos sabemos que no es lo suficientemente bueno), podría tener una función como esta.

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

Observe algunas cosas sobre esto.

  • La función de predicado en sí es consty noexcept, de acuerdo con la directriz, las afirmaciones no tendrán efectos secundarios. Si tiene sentido, también lo declara constexpr.
  • El predicado no afirma nada en sí mismo. Está destinado a ser llamado aserciones internas , como 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.
  • El flujo de control dentro de la función se divide en varias ifdeclaraciones con returns 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.

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

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::vectorfunciona 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.

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

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.

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

Esto es mejor que la siguiente solución que no le dirá si sus suposiciones eran correctas.

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

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 ...

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

…en lugar de eso.

assert(server_reachable());

Además, nunca utilizar afirmaciones para validar la entrada de confianza o se compruebe que std::mallocno lo hicieron returnque el nullptr. 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, como throwuna 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 para assert, 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.

5gon12eder
fuente
44
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.
Blrfl
4

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
44
No entiendo su punto de vista cuando dice "eso generalmente es una condición previa de la clase propietaria del mensaje o es un error fatal que debería devolverlo al usuario". Así que verifíquelo una vez, muy pronto, luego asúmalo ”. ¿Para qué utiliza las afirmaciones si no es para verificar sus suposiciones?
5gon12eder
4

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).

Mar
fuente
3

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:

  • asegurándose de que no ocurran efectos secundarios en la afirmación (proporcionada por una construcción como se explicó anteriormente)
  • rendimiento durante las pruebas de desarrollo; esto se puede solucionar agregando niveles (como el registro) a la función de aserción; para que pueda desactivar algunas afirmaciones de una compilación de desarrollo para mejorar el rendimiento
acechador
fuente
2
Santa mierda, te gusta la palabra "cierto" y sus derivaciones. Cuento 8 usos.
Casey Patton
sí, lo siento, tiendo a decir demasiado sobre las palabras - arreglado, gracias
lurscher
2

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.

Trabajo
fuente
Si su código falla 15 afirmaciones por minuto, creo que hay un problema mayor involucrado. Las afirmaciones nunca deberían dispararse en un código libre de errores y, si lo hacen, deberían matar la aplicación para evitar daños adicionales o dejarlo en un depurador para ver qué está sucediendo.
5gon12eder
2

¡Quiero trabajar contigo! Alguien que escribe mucho assertses 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 assertsi aún no lo está haciendo, así:

assert(n >= 0 && n < num && "Index is out of bounds.");

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, assertpuede ser mal utilizado e introducir errores, así:

assert(foo() && "Call to foo failed!");

... 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:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

... 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, no operator[]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
1
Bueno, puede haber "demasiadas" afirmaciones en teoría, aunque ese problema se vuelve obvio realmente rápido: si la afirmación lleva mucho más tiempo que la carne de la función. Es cierto que todavía no recuerdo haber descubierto que en la naturaleza, el problema opuesto es frecuente.
Deduplicador
@Dupuplicator Ah sí, me encontré con ese caso en esas rutinas matemáticas de vectores críticos. ¡Aunque definitivamente parece mucho mejor errar del lado de muchos que de muy pocos!
-1

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.

gnasher729
fuente
-3

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.

Arabi Cucky
fuente
3
Esto no parece ofrecer nada sustancial sobre los puntos formulados y explicados en 8 respuestas anteriores
mosquito