C++26 Contracts: precondiciones en el código

C++26 Contracts: los invariantes se mudan a la API

C++26 incorpora tres construcciones nuevas al lenguaje: pre(...), post(...) y contract_assert(...). Detrás está P2900, Contracts for C++, la propuesta que llevó la idea al estándar. El cambio de fondo es de diseño, no de sintaxis: los supuestos de una función dejan de vivir escondidos en el cuerpo y pasan a la declaración, donde cualquiera que lea la API los ve sin abrir el .cpp.

El punto de partida es el patrón que casi todo equipo de C++ tiene en producción:

int get_value(const std::vector<int>& values, std::size_t index) {
    assert(index < values.size());
    return values[index];
}

Funciona, pero arrastra dos problemas. El primero: el requisito vive en la implementación, así que quien lee solo la declaración ve un vector y un index, sin la relación entre ambos. El segundo: definir NDEBUG desactiva el chequeo y prácticamente lo borra del código.

¿Y esto cómo se aplica en tu negocio?

En CAR, dueños de empresa de Latinoamérica y España usan la tecnología para ser más rentables. Empiezas con una ruta de 7 días y terminas con un sistema funcionando en tu negocio.

👥 Probar 7 días

Con contracts, ese mismo requisito se mueve a la firma:

int get_value(const std::vector<int>& values, std::size_t index)
    pre(index < values.size())
{
    return values[index];
}

Y también puede declararse directamente en el header, que es donde más rinde:

int get_value(const std::vector<int>& values, std::size_t index)
    pre(index < values.size());

¿En qué se diferencian los contracts de un assert?

A primera vista, pre(index < values.size()) parece otra forma de escribir assert(index < values.size()). No lo es. Hay tres diferencias que importan en el día a día:

  • Requisitos en la declaración. pre y post se escriben en la firma de la función, visibles para cualquiera que lea la API sin mirar la implementación.
  • Chequeo más flexible. assert se prende o se apaga con NDEBUG. Los contracts definen varias semánticas de evaluación: ignorar el chequeo, reportar la violación y continuar, o terminar el programa. La implementación del compilador determina cuáles están disponibles y cómo se seleccionan.
  • Precondiciones y postcondiciones separadas. Los contracts distinguen lo que el llamador debe aportar de lo que la función promete devolver. Con assert habría que colocar los chequeos a mano dentro del cuerpo, incluyendo uno antes de cada return.

Ese último punto es el que convierte a los contracts en una herramienta de documentación de API, incluso cuando el chequeo en runtime está desactivado.

Postcondiciones: qué promete tu función cuando retorna

Las precondiciones describen lo que el llamador debe cumplir. Las postcondiciones describen lo que la función garantiza cuando retorna normalmente. El ejemplo canónico es un clamp:

int clamp_value(int value, const int low, const int high)
    pre(low <= high)
    post(result: result >= low && result <= high)
{
    if (value < low)
        return low;
    if (value > high)
        return high;
    return value;
}

Dos piezas útiles aquí. pre(low <= high) verifica que los límites formen un intervalo válido. post(result: ...) verifica que el valor devuelto caiga dentro de ese intervalo. El nombre result lo elige quien escribe el código y refiere al resultado de la función dentro de esa postcondición.

La ventaja práctica es que una sola postcondición cubre todas las rutas de retorno normal: no hay que repetir la aserción antes de cada return.

Las cuatro semánticas de evaluación

C++26 define cuatro semánticas, y entenderlas es lo que evita falsas expectativas:

  • Ignore: el predicado no se evalúa. No hay chequeo en runtime.
  • Observe: una violación invoca el handler; la ejecución continúa si el handler retorna normalmente.
  • Enforce: una violación invoca el handler; la ejecución termina si el handler retorna normalmente.
  • Quick-enforce: una violación termina la ejecución sin invocar el handler.

El detalle crítico: es la implementación la que determina qué semántica aplica. C++26 no tiene sintaxis portable para forzar que una aserción individual use una política específica.

Con ignore no hay chequeo. Con observe, la violación se reporta pero la ejecución sigue si el handler retorna normalmente. En el ejemplo del get_value, ambas políticas pueden dejar que el programa llegue a values[index] con un índice inválido, lo que deriva en comportamiento indefinido. Dicho de otro modo: agregar pre(...) expresa el requisito, pero la política seleccionada determina cómo responde el chequeo.

Cómo compilar contracts hoy

A octubre de 2026, solo GCC 16 soporta contracts, y se activan con banderas específicas:

g++ -std=c++26 -fcontracts=enforce example.cpp

Con esa política, la llamada inválida invoca el handler de violación. Si el handler retorna normalmente, la ejecución termina antes de que el cuerpo de la función haga el acceso inválido. El handler por defecto de GCC emite información de diagnóstico.

GCC también permite reemplazar el handler por defecto para registrar un mensaje propio. El handler recibe un objeto std::contracts::contract_violation con información sobre el contrato fallido: el predicado, la ubicación y la semántica de evaluación.

#include <contracts>
#include <print>

void handle_contract_violation(
    const std::contracts::contract_violation& violation)
{
    std::println(stderr, "Contract violated: {}",
                 violation.comment());
}

Reemplazar el handler no cambia lo que pasa después de que retorna: con observe la ejecución continúa, con enforce el programa termina. Además, no todas las implementaciones están obligadas a permitir reemplazar el handler: es un comportamiento definido por la implementación.

Los detalles que rompen migraciones

Hay dos trampas concretas al pasar de assert a contracts.

La primera son los parámetros const en postcondiciones. Si una postcondición usa un parámetro pasado por valor, ese parámetro debe tener tipo const en todas las declaraciones. En el ejemplo de clamp, low y high son const justamente por eso. Si se quitan, GCC rechaza la compilación con un error que indica que un parámetro usado en una postcondición debe ser const. El parámetro value no tiene ese requisito porque solo se usa en el cuerpo.

La segunda es la mutación dentro de predicados. Un pre(++count > 0) no compila: los predicados aplican reglas especiales de constificación, así que count se trata como const dentro del predicado. Pero eso no vuelve a los predicados libres de efectos secundarios: un predicado todavía puede llamar a una función que modifique estado global. Como la evaluación puede ignorarse y los efectos secundarios no están garantizados ni bajo semánticas de chequeo, los predicados no deberían modificar el estado del programa.

Contracts no son validación de entrada

Este es el error conceptual más caro. Supón que un índice llega desde un archivo de configuración. Si un valor inválido requiere un mensaje de error, ese manejo pertenece al flujo de control ordinario:

if (index >= values.size()) {
    std::println("Invalid index: {}", index);
    return;
}
std::println("{}", get_value(values, index));

La validación maneja un fallo esperado en el borde de la aplicación. La precondición documenta los requisitos internos del helper. P2900 lo dice explícitamente en su Principio 12: las aserciones de contrato no son control de flujo, y nada garantiza un comportamiento en runtime asociado a esa construcción sintáctica. Por eso las aserciones de contrato no deben usarse para manejo de errores ni validación de entrada: si una función tiene requisitos de reportar ciertos eventos como errores, ese manejo va con sentencias de control estándar, nunca con aserciones de contrato.

Queda un tercer punto, el del costo. Comparar un índice es barato; verificar si una colección entera está ordenada obliga a recorrer sus elementos. Un chequeo caro puede ser útil durante las pruebas, pero conviene medir su efecto sobre cargas de trabajo representativas.

¿Qué significa esto para tu startup?

Si tu producto corre sobre C++ —infraestructura, backend de baja latencia, procesamiento de datos, videojuegos, sistemas embebidos—, los contracts tocan directamente el costo de tus bugs. Un índice fuera de rango que en producción es un crash silencioso pasa a ser un requisito explícito en la API, visible en el header y verificable con una bandera de compilación.

Tres acciones concretas para empezar:

  • Audita tus headers y mueve los invariantes a la declaración. Toma las funciones donde hoy hay un assert escondido en el .cpp y expresa la relación en la firma con pre(...). El beneficio inmediato no es el chequeo en runtime, es que el contrato queda documentado para todo el equipo y para el próximo que lea esa API.
  • Separa validación de entrada de precondiciones internas. Revisa dónde estás usando assert para manejar errores esperados (config inválida, input de usuario, respuestas de red). Eso va en flujo de control normal con mensaje de error; los contracts quedan para invariantes internos.
  • Mide antes de adoptar predicados caros. Si vas a verificar propiedades costosas, como que una colección esté ordenada, hazlo primero en pruebas y mide el impacto en cargas representativas antes de llevarlo a un build de producción.

Además, conviene planificar la adopción con realismo: a octubre de 2026 solo GCC 16 soporta la feature, así que la portabilidad entre compiladores todavía es una restricción de negocio, no solo técnica. Un equipo que mantiene builds en Clang o MSVC no puede depender de contracts para lógica crítica. La lectura correcta es usarlos como documentación ejecutable y chequeo en desarrollo, no como mecanismo de seguridad en producción.

Conclusión

C++26 contracts da tres formas de expresar supuestos y garantías dentro del lenguaje: pre(...) para lo que debe cumplirse al entrar a una función, post(...) para lo que se garantiza al retornar normalmente, y contract_assert(...) para propiedades en puntos internos. La primera declaración establece el contrato, lo que convierte al header en el lugar natural para documentar los requisitos de una API.

La advertencia central es que una aserción de contrato no implica que el chequeo se ejecute siempre: la semántica seleccionada puede ignorarla, observarla, aplicarla o terminarla de inmediato. Por eso el código no debe depender de los predicados para lógica de aplicación ni para efectos secundarios. Y por eso tampoco reemplazan la validación de entrada: los errores que tu programa espera y necesita manejar van en flujo de control normal.

Fuentes

No se encontraron datos adicionales verificables en el research complementario.

¿Y esto cómo se aplica en tu negocio?

En CAR, dueños de empresa de Latinoamérica y España usan la tecnología para ser más rentables. Empiezas con una ruta de 7 días y terminas con un sistema funcionando en tu negocio.

👥 Probar 7 días

Daily Shot: Tu ventaja táctica

Lo que pasó en las últimas 24 horas, resumido para que tú no tengas que filtrarlo.

Suscríbete para recibir cada mañana la curaduría definitiva del ecosistema startup e inversionista. Sin ruido ni rodeos, solo la información estratégica que necesitas para avanzar:

  • Venture Capital & Inversiones: Rondas, fondos y movimientos de capital.
  • IA & Tecnología: Tendencias, Web3 y herramientas de automatización.
  • Modelos de Negocio: Actualidad en SaaS, Fintech y Cripto.
  • Propósito: Erradicar el estancamiento informativo dándote claridad desde tu primer café.

📡 El Daily Shot Startupero

Noticias del ecosistema startup en 2 minutos. Gratis, todos los días.

Share to...