What is the difference between expects and assert in JavaScript?

Contratos en C++: `expects` vs `assert`

24/07/2022

Valoración: 4.05 (12436 votos)

El manejo de errores es uno de los pilares fundamentales en el desarrollo de software robusto y confiable. Tradicionalmente, en C++ hemos dependido de códigos de retorno, excepciones o aserciones simples. Sin embargo, el horizonte del lenguaje se expande con conceptos más potentes, directamente inspirados en el paradigma de Diseño por Contrato. Aunque su inclusión formal en el estándar se ha pospuesto más allá de C++20, la propuesta de contratos introdujo una sintaxis y una semántica que prometen cambiar radicalmente cómo definimos las interfaces y validamos la lógica de nuestro código. En este artículo, exploraremos a fondo este sistema, centrándonos en la diferencia fundamental entre precondiciones (`expects`) y aserciones (`assert`), dos de sus componentes clave.

What is the difference between expects and assert in JavaScript?
The attribute expects is a precondition, the attribute ensures is a postcondition, and the attribute assert is an assertion. The contracts for the function push are that the queue is incomplete before adding an element that is not empty after adding, and the assertion q.is_ok () holds.
Índice de Contenido

¿Qué es un Contrato en Programación?

Antes de sumergirnos en el código, es crucial entender el concepto. Un contrato, en el contexto del software, es un conjunto de reglas y garantías que se establecen para un componente, como una función o un método. Estas reglas especifican de manera precisa y verificable las responsabilidades tanto de quien llama a la función (el cliente) como de la propia función (el proveedor). Si el cliente cumple con sus obligaciones, la función garantiza que cumplirá con las suyas. Este paradigma se materializa a través de tres elementos principales:

  • Precondiciones (`[[expects]]`): Son predicados que deben ser verdaderos antes de que se ejecute el cuerpo de una función. Representan la obligación del cliente. Si llamas a una función `push` en una cola, una precondición podría ser que la cola no esté llena.
  • Postcondiciones (`[[ensures]]`): Son predicados que deben ser verdaderos después de que la función ha terminado su ejecución. Representan la garantía del proveedor. Siguiendo el ejemplo anterior, una postcondición para la función `push` podría ser que la cola ya no esté vacía.
  • Aserciones (`[[assert]]`): Son predicados que deben ser verdaderos en un punto específico dentro de la ejecución de una función. Se utilizan para verificar invariantes o estados intermedios que son cruciales para la lógica interna del algoritmo.

Veamos un ejemplo conceptual que ilustra estos tres elementos en acción:

int push(queue& q, int val) [[expects: !q.full()]] // Precondición: El cliente no debe llamar a push en una cola llena. [[ensures: !q.empty()]] // Postcondición: La función garantiza que la cola no quedará vacía. { // ... lógica para añadir el elemento ... [[assert: q.is_ok()]]; // Aserción: En este punto, el estado interno de la cola debe ser consistente. // ... más lógica ... return ...; }

`expects` vs `assert`: La Diferencia Clave está en el Alcance

A primera vista, `expects` y `assert` pueden parecer similares: ambos verifican que una condición sea verdadera. Sin embargo, su diferencia es fundamental y radica en dónde viven: en la interfaz pública o en la implementación privada.

`[[expects]]`: Parte de la Interfaz Pública

Una precondición, definida con `[[expects]]`, es parte del contrato público de una función. Es una regla que el código cliente debe conocer y respetar. Debido a esto, una precondición solo puede acceder a información que es públicamente visible desde el exterior de la función o clase. No puede, por ejemplo, acceder a variables locales de la función (porque aún no existen) ni a miembros privados o protegidos de una clase, ya que estos son detalles de implementación ocultos al cliente.

Consideremos el siguiente ejemplo, que ilustra esta restricción:

class MiClase { public: void procesar(int n) [[expects: n < m]] // ¡ERROR DE COMPILACIÓN! 'm' es privado. { // ... } private: int m = 100; };

Este código no compilaría porque la precondición `n < m` intenta acceder al miembro privado `m`. Desde la perspectiva del contrato, `m` es un detalle de implementación que el cliente no conoce y, por lo tanto, no puede formar parte de las condiciones que se le exigen.

`[[assert]]`: Parte de la Implementación Interna

Una aserción, definida con `[[assert]]`, vive dentro del cuerpo de la función. Es una herramienta para el desarrollador de la función, no para el cliente. Su propósito es validar suposiciones sobre el estado interno del programa a medida que el algoritmo progresa. Dado que opera dentro de la implementación, una aserción sí puede acceder a todo lo que la función puede ver: variables locales, parámetros y miembros privados o protegidos de la clase.

Modifiquemos el ejemplo anterior para usar `assert` correctamente:

class MiClase { public: void procesar(int n) { [[assert: n < m]]; // ¡CORRECTO! 'm' es accesible desde aquí. // Lógica que depende de que 'n' sea menor que 'm' } private: int m = 100; };

En este caso, la aserción es válida. No es una condición que el cliente deba cumplir, sino una verificación interna que el programador de `MiClase` ha puesto para asegurarse de que su propia lógica se mantiene consistente. Si esta aserción falla, indica un error de programación (un bug) dentro de la clase, no un uso incorrecto por parte del cliente.

Sintaxis y Modificadores de Contratos

La propuesta de contratos define una sintaxis flexible que permite controlar el coste y el comportamiento de las verificaciones. La estructura general es:

[[atributo-de-contrato modificador: expresión-condicional]]

  • Atributo de contrato: Puede ser `expects`, `ensures` o `assert`.
  • Modificador: Especifica el nivel o el modo de cumplimiento del contrato.
  • Expresión condicional: El predicado que debe evaluarse como verdadero.

Los modificadores son clave para gestionar el impacto en el rendimiento:

ModificadorDescripciónCoste en Tiempo de Ejecución
defaultEs el modificador por defecto si no se especifica. Las comprobaciones deben tener un bajo coste computacional. Están pensadas para estar activas incluso en compilaciones de producción.Bajo
auditPara comprobaciones que pueden ser más costosas. Están pensadas para activarse en compilaciones de depuración, pruebas o auditorías de seguridad, pero no en producción.Potencialmente alto
axiomEl predicado se asume como verdadero. No se realiza ninguna comprobación en tiempo de ejecución. Sirve como documentación para el programador y como información para las herramientas de análisis estático y el optimizador del compilador.Nulo

Para la postcondición `ensures`, existe una sintaxis adicional que permite nombrar el valor de retorno de la función para usarlo en el predicado:

int multiplicar(int x, int y) [[expects: x > 0]] [[expects default: y > 0]] [[ensures audit res: res > 0]] // 'res' es un identificador para el valor retornado { return x * y; }

Manejo de Violaciones de Contrato

¿Qué sucede cuando un contrato se rompe? Por defecto, la violación de un contrato se considera un error lógico irrecuperable que debe resultar en la terminación del programa. Cuando un predicado de contrato evalúa a `false`, se invoca un manejador de violaciones.

What is the STD expected class template in C++?
The `std::expected` class template is defined in the C++ standard library, allowing you to specify two types: one for the successful result (denoted as `T`) and another for the error state (denoted as `E`). This provides a clean and unified way to handle function outcomes.

El compilador suele ofrecer tres niveles de construcción de aserciones:

  1. off: No se comprueba ningún contrato. Máximo rendimiento.
  2. default: Se comprueban los contratos marcados como `default` (o sin modificador). Este es el nivel por defecto.
  3. audit: Se comprueban tanto los contratos `default` como los `audit`.

El manejador de violaciones por defecto llama a `std::terminate`, pero los programadores pueden establecer su propio manejador. Este manejador recibe un objeto `std::contract_violation` que contiene información detallada sobre el fallo:

  • line_number(): Línea donde se definió el contrato fallido.
  • file_name(): Nombre del archivo.
  • function_name(): Nombre de la función.
  • comment(): El texto del predicado que falló.
  • assertion_level(): El nivel del contrato (`default`, `audit`).

Preguntas Frecuentes (FAQ)

¿Son los contratos un reemplazo para las excepciones?

No, cumplen propósitos diferentes. Los contratos están diseñados para detectar errores de programación (bugs) que nunca deberían ocurrir en un programa correcto. Una violación de contrato es irrecuperable. Las excepciones (o alternativas como `std::expected`) se usan para manejar errores de tiempo de ejecución que son esperables y potencialmente recuperables, como un archivo no encontrado, una conexión de red fallida o una entrada de usuario inválida.

¿Qué impacto tienen los contratos en el rendimiento?

El impacto depende directamente del nivel de construcción (`off`, `default`, `audit`) y de los modificadores usados. En el nivel `off` o con contratos `axiom`, el impacto es nulo. Los contratos `default` están diseñados para ser lo suficientemente ligeros como para usarse en producción, mientras que los `audit` pueden tener un coste mayor y se reservan para fases de prueba y depuración.

¿Puedo personalizar lo que sucede cuando se viola un contrato?

Sí, es posible instalar un manejador de violaciones personalizado. Sin embargo, la función manejadora debe ser `noexcept`, lo que significa que no puede lanzar excepciones. Su propósito principal es registrar información detallada sobre el error antes de que el programa termine, no intentar recuperar el flujo de ejecución.

¿En qué versión de C++ están disponibles los contratos?

Esta es una pregunta importante. Los contratos fueron una de las características más esperadas para C++20, pero debido a complejidades en su especificación, su inclusión en el estándar fue pospuesta. Las propuestas citadas (P0380R1, P0542R5) sirvieron de base, pero la característica volverá a ser considerada para futuras versiones de C++, como C++26 o posterior. A pesar de ello, el estudio de este sistema sigue siendo inmensamente valioso, ya que representa la dirección futura del diseño de software en C++.

Conclusión: Un Vistazo al Futuro de la Robustez

Aunque todavía no formen parte del estándar, los contratos representan una evolución monumental en la forma en que pensamos sobre las interfaces y el manejo de errores en C++. La distinción clara entre `expects` como parte de la interfaz pública y `assert` como una herramienta de la implementación interna nos obliga a ser más disciplinados y explícitos sobre las responsabilidades de nuestro código. Al integrar estas validaciones directamente en el lenguaje, creamos software más seguro, auto-documentado y fácil de depurar. Como dijo Herb Sutter, los contratos son potencialmente una de las adiciones más impactantes al lenguaje desde C++11. Estar preparados para su llegada es prepararse para escribir una nueva generación de código C++ más robusto y confiable que nunca.

Si quieres conocer otros artículos parecidos a Contratos en C++: `expects` vs `assert` puedes visitar la categoría Juegos.

Subir