La elección del tipo de dato para representar tamaños y longitudes en lenguajes de programación de sistemas, como size_t en C, tiene profundas implicaciones en la seguridad, la legibilidad y la propensión a errores del código. Históricamente, muchos lenguajes han optado por tipos sin signo (unsigned) bajo la premisa de que los tamaños no pueden ser negativos y para maximizar el rango de valores positivos. Sin embargo, esta decisión introduce una serie de "trampas" (footguns) relacionadas con el desbordamiento (overflow), la promoción de tipos en expresiones mixtas y la dificultad de manejar operaciones aritméticas como el módulo, que se manifiestan en bugs sutiles y difíciles de depurar.
El problema fundamental radica en que la aritmética sin signo se comporta como aritmética modular por definición, lo que es útil para ciertos casos criptográficos o de hashing, pero contraproducente para la representación de cantidades físicas o lógicas como tamaños y offsets, donde un desbordamiento o un valor negativo inesperado debería ser una condición de error clara. La decisión de C3 de adoptar tipos con signo por defecto para tamaños es una reevaluación de este compromiso, priorizando la seguridad y la previsibilidad del comportamiento aritmético sobre la maximización del rango, alineándose con las decisiones de diseño de lenguajes como Java y Go.
Arquitectura del Sistema
El cambio en C3 implica la redefinición del tipo por defecto para tamaños y longitudes de usz (anteriormente usize) a sz (equivalente a ssize_t en POSIX). Esta modificación afecta directamente la API de las funciones que operan con colecciones, buffers y estructuras de datos que requieren indexación o cálculo de offsets. La arquitectura del compilador de C3 se adapta para eliminar las conversiones implícitas entre tipos con y sin signo, forzando la explicitud cuando tales conversiones son necesarias. Esto reduce la superficie de ataque para errores de promoción de tipos, donde una expresión signed + unsigned podría comportarse de manera inesperada.
La decisión también simplifica el manejo de operaciones como el operador módulo (%), que en C y C++ puede tener un comportamiento diferente para operandos negativos (produciendo un resto en lugar de un módulo matemático). Al trabajar predominantemente con tipos con signo, el manejo de offsets negativos en estructuras como ring buffers se vuelve más intuitivo y menos propenso a errores, ya que la aritmética de complemento a dos para enteros con signo maneja naturalmente los valores negativos hasta el límite de INT_MIN. La eliminación de conversiones implícitas y la preferencia por sz simplifican el razonamiento sobre el flujo de datos y el comportamiento aritmético en el código base, reduciendo la necesidad de patrones de codificación complejos para mitigar los problemas de unsigned.
Trade-offs
Ganancias
- ▲ Reducción de bugs relacionados con desbordamientos y comparaciones de tipos
- ▲ Simplificación de la aritmética de enteros, especialmente con el operador módulo
- ▲ Mayor claridad y previsibilidad en el comportamiento del código
Costes
- △ Menor rango máximo de valores positivos para tamaños (en máquinas de 32 bits)
- △ Necesidad de conversiones explícitas para casos donde se requiere aritmética modular o un rango extendido
/* Con signed (sz) */
idx = (offset + capacity) % capacity;
/* Con unsigned (usz) - incorrecto para offsets negativos */
idx = (offset + capacity) % capacity;
/* Con unsigned (usz) - correcto, pero más complejo */
idx = (offset % capacity + capacity) % capacity;Fundamentos Teóricos
La discusión sobre los tipos con y sin signo y sus implicaciones en la seguridad y corrección del software se remonta a los orígenes de los lenguajes de programación de sistemas. El estándar C original, y posteriormente C++, adoptó size_t como un tipo sin signo para la salida del operador sizeof, una decisión que, como se argumenta en el artículo, introdujo la aritmética sin signo en el uso común. Este problema se relaciona con la teoría de tipos y la seguridad de tipos, donde un sistema de tipos robusto busca prevenir clases enteras de errores en tiempo de compilación o ejecución.
La elección de Java de eliminar completamente los tipos sin signo, y la de Go de usar tipos con signo para tamaños, son ejemplos de cómo los principios de diseño de lenguajes pueden priorizar la seguridad y la simplicidad sobre la flexibilidad o el rango máximo. Conceptos como la aritmética modular (que subyace a la aritmética sin signo) son bien conocidos en criptografía y teoría de números, pero su aplicación indiscriminada a la representación de tamaños en la programación de sistemas ha demostrado ser una fuente de errores. La dificultad de razonar sobre el desbordamiento de enteros y las reglas de promoción de tipos en expresiones mixtas es un tema recurrente en la investigación de lenguajes de programación y la verificación formal de software, donde se busca garantizar la corrección de programas a través de propiedades matemáticas.