Todo en C es comportamiento indefinido
(blog.habets.se)- El comportamiento indefinido (UB) no es una optimización maliciosa del compilador, sino una regla que le permite no manejar rutas de ejecución imposibles bajo la suposición de que el código es válido
- En el código C/C++ no trivial hay UB oculto por todas partes, no solo
double-freeo accesos fuera de límites, sino también en alineación, conversiones de tipo, inicialización y discrepancias de tipos - Acceder a un
int*ostd::atomic<int>*desalineado ya es UB según el estándar, aunque según la plataforma pueda terminar enSIGBUS, una corrección del kernel o algo que parezca funcionar normalmente - Incluso código común como pasar un
charcon signo aisxdigit(), convertirfloataint, o usar malNULLy argumentos variables puede salirse fácilmente de lo permitido por el estándar - No se pueden desechar las bases de código existentes, pero habrá que corregir esto a gran escala combinando detección de UB con LLM y validación de expertos, porque es demasiado sutil para dejárselo a personal junior
El comportamiento indefinido en C/C++ no es un problema de optimización
- El comportamiento indefinido (UB) no significa que el compilador “aproveche” errores del desarrollador, sino que puede asumir que el programa es válido según el estándar
- Aunque para una persona la intención parezca clara, esa intención puede ser difícil de expresar entre fases del compilador o entre módulos
- El compilador no tiene obligación de generar código que maneje casos especiales que “no pueden ocurrir”, y el resultado puede diferir de lo esperado en la ruta de ejecución real, incluido el hardware
- Desactivar optimizaciones no vuelve seguro el UB, ni hay garantía de que el mismo comportamiento se mantenga en compiladores o arquitecturas presentes o futuras
El UB no solo existe en código anormal
- double-free, use-after-free, acceso fuera de los límites de un objeto y acceso a memoria no inicializada son UB conocidos, pero siguen apareciendo en toda la industria
- También hay mucho UB más sutil y contraintuitivo, por lo que código C/C++ aparentemente normal puede salirse fácilmente del estándar
- La norma C23 contiene 283 apariciones de la palabra “undefined”, y el alcance es aún mayor si se incluyen casos no especificados que terminan siendo indefinidos
- En cualquier código C/C++ no trivial hay UB por todos lados, y es difícil atribuirlo solo al descuido de programadores individuales
Acceso a objetos desalineados
- Una función que desreferencia un
int*como la siguiente incurre en UB si el puntero no está correctamente alineadoint foo(const int* p) { return *p; } - La alineación (alignment) suele significar una dirección múltiplo de
sizeof(int), pero los requisitos reales pueden variar según la plataforma y la implementación - En Linux Alpha, en algunos casos el kernel podía atrapar la excepción y emular por software el acceso esperado, pero en otros el programa podía morir con
SIGBUS - En SPARC ocurre
SIGBUS, mientras que en x86/amd64 normalmente parece funcionar sin problema o incluso como una lectura atómica - En ARM, RISC-V o arquitecturas futuras no se puede generalizar el resultado, y una arquitectura futura podría usar bits bajos de
int*para registros especiales - Si el compilador usa una instrucción de carga distinta, un acceso que antes el kernel corregía podría dejar de corregirse
- El compilador no está obligado a generar ensamblador que funcione con punteros desalineados, porque ese acceso en sí mismo ya es UB
Los tipos atómicos también son UB si están mal alineados
- Incluso si se llama a
store()oload()sobre unstd::atomic<int>*como sigue, el comportamiento es UB si el objeto no está correctamente alineadovoid set_it(std::atomic<int>* p) { p->store(123); } int get_it(std::atomic<int>* p) { return p->load(); } - Desde la perspectiva del estándar, la pregunta de si esa operación es atómica sobre un objeto desalineado ni siquiera aplica
- En hardware real la atomicidad puede ser un problema, pero según el estándar ya era UB antes de llegar a eso
- Si el objeto que se cree leer atómicamente cruza una página, el problema se vuelve más complejo, pero la conclusión no es “está bien”, sino UB
Solo crear el puntero ya puede ser un problema
- Con un puntero desalineado, incluso antes de desreferenciarlo, convertirlo a un puntero de cierto tipo ya puede ser problemático
bool parse_packet(const uint8_t* bytes) { const int* magic_intp = (const int*)bytes; // UB! int magic_raw = foo(magic_intp); // Probably crashes on SPARC. int magic = ntohl(magic_raw); // this is fine, at least. […] } - Aquí el problema no es la llamada a
foo(), sino el cast(const int*)bytes - Según el estándar, también es posible que el compilador dé significado a los bits bajos de
int*, como bits de recolección de basura o etiquetas de seguridad
El problema de pasar char a isxdigit()
- El siguiente código parece simple, pero puede ser UB en arquitecturas donde
chares signed si el valor de entrada sale del rango 0–127bool bar(char ch) { return isxdigit(ch); } isxdigit()es una función que verifica si un carácter es hexadecimal, y también puede recibirEOF- Según C23 7.4p1,
EOFes de tipoint, por lo que se puede inferir que es un valor no representable comounsigned char isxdigit()recibeint, nochar, y aunque la conversión decharaintes posible, los valores negativos designed charson el problema- Según C23 6.2.5 párrafo 20, que
charsea signed o no depende de la implementación - Una implementación de
isxdigit()como la siguiente puede leer memoria desconocida con un índice negativoint isxdigit(int c) { if (c == EOF) { return false; } return some_array[c]; } - Si esa memoria pertenece a una región mapeada a I/O, podría no solo devolver valores arbitrarios o provocar un crash, sino incluso activar comportamiento de hardware
- Eso es más probable en sistemas embebidos que en aplicaciones sobre sistemas operativos de escritorio, pero también hay casos en espacio de usuario donde la protección no basta, como drivers de red en espacio de usuario
El problema de convertir float a int
- Código como el siguiente, que convierte segundos en
floata milisegundos enint, es común pero contiene UBint milliseconds(float seconds) { int tmp = (int)(seconds * 1000.0); /* WRONG */ return tmp + 1; /* WRONG separately (signed overflow is UB) */ } - C23 6.3.1.4 establece que al convertir un valor finito de punto flotante real a un tipo entero, si la parte entera no puede representarse en ese tipo entero, el comportamiento es indefinido
- Para valores no finitos tampoco hay una definición explícita, así que también es UB
- Incluso comparar un
floatconINT_MAXno es tan simple- Hacer cast de
floataintpuede disparar justamente el UB que se intenta evitar - Hacer cast de
INT_MAXafloatno garantiza una representación exacta - Si
INT_MAXse redondea enfloata un valor ya no representable comoint, la comparación deja de ser fiable
- Hacer cast de
- Para hacerlo seguro hacen falta una verificación con
isfinite(), comparaciones con margen comoINT_MIN + 1000yINT_MAX - 1000, y una comprobación adicional antes de sumar después de la conversiónint milliseconds(float seconds) { const float ftmp = seconds * 1000.0f; if (!isfinite(ftmp)) { return 0; } if ((float)(INT_MIN + 1000) > ftmp) { return 0; } if ((float)(INT_MAX - 1000) < ftmp) { return 0; } const int tmp = (int)ftmp; if (INT_MAX == tmp) { return 0; } return tmp + 1; } - Uno solo quiere convertir
floataint, pero el código seguro termina siendo mucho más largo
Objetos en la dirección 0 y null pointer
- En kernels de SO o código embebido pueden aparecer situaciones donde se quiera colocar un objeto en la dirección 0
- En la práctica, puede decirse que no hay una forma útil de colocar realmente un objeto en la dirección 0 cumpliendo con el estándar de C
- En C 6.3.2.3, la constante entera 0 convertible a puntero y
nullptrson “null pointer constant”, y aquí se les puede llamarNULL - C no especifica que un puntero
NULLreal apunte a la dirección de máquina 0 - El estándar de C trata con la máquina abstracta de C, no con el hardware, y solo garantiza que
NULLy 0 se comparan como iguales - Esa igualdad podría deberse a que el entero 0 se convierte al valor nativo de
NULLde esa plataforma, y ese valor incluso podría ser0xffff - Desreferenciar un null pointer es UB sin importar cuál sea su valor, y es un ejemplo representativo en C 3.4.3
- Por eso no se puede asumir que
memset(&ptr, 0, sizeof(ptr));crea un punteroNULL - Inicializar una estructura con ceros y asumir que sus punteros miembro son
NULLes algo que también causa problemas reales a la mayoría de programadores - Históricamente también existieron máquinas con punteros
NULLdistintos de 0
El problema de asumir que hay una función en la dirección 0
- Incluso si en una máquina moderna
NULLapunta a la dirección 0 y realmente existe un objeto o función ahí, C 6.3.2.3 establece queNULLno es igual a ningún objeto ni función - Por lo tanto, el siguiente código es UB
void (*func_ptr)() = NULL; func_ptr(); - Desde la perspectiva de C, eso significa “no hay ninguna función ahí”, y puede que no exista manera de expresar otra intención dentro del compilador
- No se puede asumir simplemente que se emitirá una instrucción
calla una dirección cuyos bits sean todos 0 - En x86 de 16 bits ni siquiera está claro si “todos 0” significa
0000:0000oCS:0000
Argumentos variables y discrepancias de tipos
- El último argumento de
execl()debe ser un puntero, así que pasar directamente la macroNULLo el entero 0 puede ser UBexecl("/bin/sh", "sh", "-c", "date", NULL); /* WRONG */ execl("/bin/sh", "sh", "-c", "date", 0); /* WRONG */ - La forma correcta es hacer cast explícito al tipo puntero
execl("/bin/sh", "sh", "-c", "date", (char*)NULL); - La macro
NULLpuede interpretarse como el entero 0, y en argumentos variables no se transmite la información de tipo necesaria - En
printf()también hay UB si el especificador de formato no coincide con el tipo real del argumentouint64_t blah = 123; printf("%ld\n", blah); /* WRONG */ - Para imprimir
uint64_thay que usarPRIu64uint64_t blah = 123; printf("%"PRIu64"\n", blah); - Para imprimir
uid_t, una opción puede ser hacer cast auintmax_ty usarPRIuMAX, aunque ni siquiera está garantizado queuid_tsea unsigned - En el peor caso, en vez de
-1podría imprimirse un valor sin sentido
División entre cero y problemas de seguridad
- Que dividir entre 0 sea UB es algo bien conocido, pero cuando el denominador proviene de una entrada no confiable eso se vuelve un problema de seguridad
- Lo importante es que no se trata solo de un simple error en tiempo de ejecución, sino de UB en el límite de validación de entrada
No es UB, pero las promociones enteras también son peligrosas
- Las reglas de promoción entera son difíciles de aplicar al ritmo de una lectura rápida del código, y pueden dar resultados contrarios a la intuición
- En el siguiente código,
overflowedno queda en 1 sino en 0unsigned char a = 0xff; unsigned char b = 1; unsigned char zero = 0; bool overflowed = (a + b) == zero; // overflowed is set to zero, not one. - En el siguiente ejemplo, aunque todas las variables parezcan unsigned, el resultado no es
2147483648 (0x80000000)sino18446744071562067968 (ffffffff80000000)unsigned char a = 0x80; uint64_t b = a << 24; // Bonus UB(?) - Aunque no sea UB, las reglas enteras de C/C++ no son intuitivas y facilitan la aparición de defectos
Detección de UB con LLM
- Los LLM modernos, cuando se les pide encontrar UB en código C arbitrario, casi siempre detectan problemas y por lo general aciertan
- Después de encontrar UB en código personal, se aplicó el mismo enfoque al código de OpenBSD, que es maduro y está escrito con bastante rigor
- Al apuntar primero a la herramienta
find, se descubrieron varios problemas - Se enviaron a OpenBSD parches por escritura fuera de rango y por un bug lógico que no era UB
- No se enviaron parches para muchos otros UB que seguían ahí
- Había experiencia previa de que el proyecto OpenBSD no había sido muy receptivo a reportes de bugs en el pasado
- También se consideró que en la práctica podía no ser grave
- Si OpenBSD quisiera eliminar UB de su base de código, haría falta un proyecto más grande que un flujo de parches individuales entre el LLM y el proyecto
Dirección realista para lidiar con bases de código C/C++
- No se pueden tirar a la basura las bases de código C/C++ existentes, pero tampoco es opción dejarlas en un estado esencialmente roto
- Hay que corregir UB a gran escala sin hacer commits de cambios de baja calidad generados por IA y sin sobrecargar a las personas revisoras
- En 2026, escribir C o C++ sin supervisión de UB por parte de un LLM podría verse como una violación de SOX o como una irresponsabilidad
- Si incluso desarrolladores de OpenBSD no lograron detectar todos estos problemas en más de 30 años, en otros proyectos las probabilidades son todavía peores
- En proyectos personales, se puede pedir a un LLM que encuentre UB, lo explique si hace falta, lo corrija y luego una persona verifique el resultado
- Aun así, para validar esos resultados hacen falta expertos, y normalmente esos expertos ya están ocupados con otras cosas
- Esto parece una tarea de limpieza, pero es demasiado sutil para dejársela a programadores junior, que tradicionalmente eran quienes recibían ese tipo de trabajo
1 comentarios
Comentarios en Hacker News
En C hay muchísimo comportamiento indefinido sorprendente y raro, pero este artículo realmente no lo muestra bien; apenas rasca la superficie
Un ejemplo aún más extraño es
volatile int x = 5; printf("%d in hex is 0x%x.\n", x, x);. Sixfuera soloint, no habría problema, pero si esvolatile, pasa a ser comportamiento indefinido. Según el estándar de C, acceder avolatileya cuenta como un efecto secundario solo con leerlo, los efectos secundarios no ordenados sobre el mismo objeto escalar son comportamiento indefinido, y la evaluación de los argumentos de una función no tiene un orden determinado entre síNormalmente, una data race significa que distintos hilos acceden al mismo objeto al mismo tiempo y al menos uno escribe, pero en C puede surgir una situación parecida a una data race incluso en un solo hilo y sin escrituras
undefineden el estándar, ni todos los casos indefinidos que surgen por omisionesEl punto es que no se puede evitar. Al menos desde que apareció C en 1972, ningún ser humano ha logrado evitarlo por completo
Si no se ha conseguido en 54 años, entonces “esfuérzate más” o “no cometas errores” no son soluciones. El defecto explotable que encontró Mythos en OpenBSD fue bastante bien valorado por los desarrolladores de OpenBSD, pero incluso al pasar herramientas sobre el código más simple aparecía una enorme cantidad de comportamiento indefinido
Por ejemplo, que
findlea la variable automática no inicializadastatusdespués dewaitpid(&status)y antes de comprobar siwaitpid()devolvió error también es comportamiento indefinido, aunque cuesta imaginar una arquitectura o compilador donde eso fuera explotableComo escribí en el artículo, no intento enumerar todo el comportamiento indefinido del mundo, sino señalar que todo código C/C++ no trivial contiene comportamiento indefinido
volatilees un hack del sistema de tipos. Debió resolverse de una manera más principista, y los lenguajes modernos no deberían copiarlo como si “C lo hizo, así que debe ser buena idea”Los primeros compiladores de C siempre volcaban los valores a memoria, así que si un puntero apuntaba a hardware de entrada/salida mapeado en memoria, cada cambio en
xgeneraba una instrucción de CPU que realmente escribía en memoria y el código del driver funcionabaPero cuando llegaron las optimizaciones, el compilador vio que solo se seguía modificando
xy empezó a dejarlo en registros, con lo cual el driver se rompió.volatileen C es un hack para decirle al compilador “esa optimización no la hagas”, mientras que la solución correcta, proveer intrinsics de E/S mapeada en memoria a nivel de biblioteca, habría sido un trabajo mucho mayorLa razón por la que hacen falta intrinsics es que permiten expresar con precisión qué operaciones son posibles y cuáles no. En algunos destinos, escrituras de 1 byte, 2 bytes y 4 bytes producen comportamientos distintos, y el hardware los distingue. Algunos dispositivos esperan una escritura RGBA de 4 bytes, y si les emites cuatro escrituras de 1 byte pueden confundirse o no funcionar. Algunos destinos incluso soportan escrituras a nivel de bits. Con solo
volatile, no hay manera de saber qué está ocurriendo ni qué significaSi compilas un programa en C y luego lo desensamblas, obtienes un programa en ensamblador sin comportamiento indefinido. Eso es porque en ensamblador no existe el concepto de comportamiento indefinido
El comportamiento indefinido es una propiedad del programa fuente, no del ejecutable. Significa que la especificación del lenguaje en que se escribió el código fuente no le asigna significado al programa. En cambio, el ejecutable compilado sí recibe significado por la especificación de la máquina
Una race condition es una propiedad del comportamiento del programa. Por eso se puede decir que un programa en C tiene comportamiento indefinido, pero no que necesariamente el ejecutable tenga una race real. Claro, el compilador puede compilar arbitrariamente un programa con comportamiento indefinido e introducir una race, pero si lo compila sin crear hilos nuevos, entonces no hay race
volatilees precisamente que el valor puede ser cambiado por otra cosa. Si es una variable global, esa otra cosa puede ser otro hilo, pero también una interrupción o un signal handler. Si es un puntero que lee una dirección concreta, podría ser un registro de dispositivo de hardware cuyo valor cambiaEl concepto de variable
volatileen sí no es el problema. Si un lenguaje quiere soportar rutinas de interrupción y E/S mapeada en memoria, necesita una forma de decirle al compilador que leer dos veces el mismo registro de hardware no es lo mismo que leer dos veces la misma ubicación de memoriaEl verdadero problema es que la interacción entre las funciones del lenguaje y sus restricciones no quedó bien resuelta. Si ya especificaste “este valor puede cambiar en cualquier momento”, entonces es absurdo considerar ciertos usos como comportamiento indefinido justamente por esa razón. Para las variables
volatiledebería haber existido una excepción en la definición de “efectos secundarios no ordenados”Mucha gente cree erróneamente que C y C++ son “muy flexibles porque te dejan hacer lo que quieras”. En realidad, casi toda técnica que parece poderosa e impresionante es un campo minado de comportamiento indefinido
El comportamiento indefinido de los punteros desalineados es peor todavía. Un puntero desalineado es comportamiento indefinido no solo al acceder a través de él, sino por el mero hecho de existir como puntero
Por eso, convertir implícitamente
void* vaint* i, por ejemplo coni=ven C o al pasarf(v)a una función que recibeint*, también es comportamiento indefinido si el puntero resultante no satisface la alineación requerida paraintEs importante que esto sea un problema a nivel de C. Si un programa en C tiene comportamiento indefinido, entonces formalmente no es válido y es un programa incorrecto. No es un problema de hardware, ni tiene que ver con crashes o fallos
La conversión de
void*aint*normalmente no genera ninguna instrucción de hardware, y como los tipos existen solo en C, el hardware tampoco se cae por ese cast. Podrías pensar que si es solo un valor entero en un registro, no pasa nada, pero el punto no es si en hardware el puntero es “realmente” un entero, sino que en el momento en que haces el cast a un puntero desalineado, el programa en C ya quedó roto por definiciónTambién quise transmitir que el comportamiento indefinido no está en el hardware y no tiene relación con crashes o fallos. Al mismo tiempo, quería mostrar ejemplos a la gente que dice “pero si se ve que funciona bien”, y en realidad no es así
#pragma pack(push, 1), ¿significa que no puedo usar punteros a miembros salvo que por casualidad estén alineados?Ese tipo de comportamiento indefinido está bien, y casi nadie considera un gran problema que aparezcan bugs por diferencias de hardware
Pero con el tiempo, interpretaciones agresivas transformaron C en una especie de lenguaje de design by contract implícito, y las restricciones quedaron invisibles. Eso crea un problema parecido al de RAII, donde las llamadas implícitas al destructor no se ven
En C, al desreferenciar un puntero, el compilador añade implícitamente una restricción de no-null a la firma de la función. Si pasas a una función un puntero que podría ser null y no hay una comprobación ni una aserción, en vez de marcar error por falta de chequeo, el compilador propaga silenciosamente esa restricción de no-null sobre el puntero. Si logra demostrar que esa restricción es falsa, marca la función como inalcanzable, y una llamada a una función inalcanzable vuelve inalcanzable también a la función que la llama
Las 5 etapas de aprender sobre comportamiento indefinido en C
Negación: “Yo sé qué pasa con el overflow con signo en mi máquina”
Ira: “¡Este compilador es basura! ¿Por qué no hace lo que le dije?”
Negociación: “Voy a mandar esta propuesta a wg14 para arreglar C…”
Depresión: “¿Hay algo de código C en lo que se pueda confiar?”
Aceptación: “Simplemente no uses comportamiento indefinido”
Los accesos desalineados se resuelven usando structs empaquetadas. El compilador genera mágicamente el código correcto. En realidad, el compilador siempre supo hacerlo bien; simplemente no lo hacía
Las reglas de strict aliasing se resuelven usando type punning con unions. En cualquier compilador importante está documentado que funciona aunque el estándar no lo diga. O si no, se desactiva con
-fno-strict-aliasing. Así puedes reinterpretar memoria como quieras, y aunque habrá bordes filosos, al menos no vendrán del compiladorEl overflow se define con
-fwrapv. Si reemplazas+,-,*por__builtin_*_overflow, además obtienes chequeo explícito de errores gratis. La interfaz funcional es buena y además genera código eficienteLa verdadera aceptación se parece más a “la gente normal no se preocupa por el estándar de C”. El estándar es pésimo y lo que importa es el compilador. Los compiladores tienen muchísimas funciones muy útiles para esquivar la mayoría de estos problemas. La razón por la que la gente no las usa es porque quiere escribir C “portable” y “estándar”, y la verdadera aceptación es salir de esa forma de pensar
Con esa lógica hice un intérprete de Lisp en C freestanding y además pasó UBSan. Al principio pensé que iba a explotar, pero no fue así, y si yo puedo hacerlo, cualquiera puede
Mientras los humanos sigan escribiendo código, eso no puede ser el estado final. Ningún ser humano puede evitar por completo el comportamiento indefinido en C/C++
Los ejemplos se parecen más a casos que pueden convertirse en comportamiento indefinido según la entrada o la situación, que a comportamiento indefinido real
Si lo defines tan ampliamente, entonces cualquier llamada a función también sería comportamiento indefinido porque puede agotarse el espacio de stack. De hecho, en casi cualquier lenguaje podría decirse algo parecido en ese sentido
C ya tiene suficientes asperezas reales y notables; este tipo de sensacionalismo puede distraer, especialmente a principiantes, y terminar siendo perjudicial
STORAGE_ERRORhttp://archive.adaic.com/standards/83lrm/html/lrm-11-01.html
Ahí dice que esta excepción también puede ocurrir “si no hay suficiente espacio de almacenamiento durante la ejecución de una llamada a subprograma”
Para empezar, sí es posible definir qué ocurre cuando se agota el espacio de stack. Además, no todos los programas requieren una pila de tamaño arbitrario; algunos solo necesitan una cantidad constante calculable de antemano. Algunas implementaciones de lenguajes ni siquiera usan pila
El lenguaje también podría ofrecer herramientas para consultar el espacio de stack restante y dar garantías en función de eso. O podría permitir instalar un manejador que se ejecute cuando se agote el stack
La manera correcta de pensarlo es que, en el instante en que ocurre comportamiento indefinido, ya no estás bajo la protección del estándar del lenguaje. Puede seguir funcionando un rato, tal vez para siempre. Pero en la práctica quedas a merced, sin saberlo, de los caprichos de la toolchain, del cambio o actualización del compilador, de la arquitectura, del runtime o de la versión de libc
Al final construyes sobre arena, y ese es el peligro del comportamiento indefinido
El problema del comportamiento indefinido no es que pueda causar un crash en alguna arquitectura
El verdadero problema es que el compilador espera que ese código nunca ocurra. Si aun así escribes código con comportamiento indefinido, el compilador, especialmente el optimizador, puede traducir el camino “normal” de cualquier forma que le resulte conveniente. Y a veces ese “cualquier forma” puede ser muy inesperado, como eliminar grandes bloques de código
Si además entra tail recursion, incluso podría darse que en build de debug no llegues al loop infinito, pero que el bug solo aparezca al subir el nivel de optimización
En casos peores, el programa puede seguir corriendo silenciosamente con valores basura, formatear el disco duro o entregarle al atacante las llaves del reino
La gente que propone simplemente definirlo o volverlo comportamiento no especificado no entiende que la clave es que el compilador pueda eliminar grandes partes del programa
Si escribes código que se vuelve comportamiento indefinido para ciertas entradas, entonces para esas entradas estás expresando que el programa no debe tener ningún comportamiento. Quieres que el compilador pueda optimizar fuera ese camino o hacer lo que ayude al comportamiento de los otros casos definidos
Se siente bastante satisfactorio meter una cadena de log que solo sería alcanzable mediante comportamiento indefinido y luego ver que la cadena ni siquiera queda en el binario
Hace tiempo escribí un pass de análisis bajo el supuesto de que correría al final del pipeline de transformaciones, y esa suposición era necesaria para la corrección. Como ya no habría más optimizaciones, me parecía seguro, pero ahora ya no estoy tan seguro
Llevo 20 años usando C, pero nunca había visto tanta conversación sobre comportamiento indefinido como en estos últimos 6 meses en Hacker News
En conversaciones reales casi nunca aparecía. Uno escribe código, y si no funciona lo depura y lo arregla o lo rodea. No entiendo por qué el tema del comportamiento indefinido en C sigue apareciendo tan seguido en portada
Siempre ha habido una minoría constante de gente de ciencias de la computación que considera que desarrollar o usar un lenguaje nuevo es lo más interesante del mundo, y algunos siguen pensándolo
Es natural que esa gente se interese por temas de diseño de lenguajes, y el comportamiento indefinido en C entra en esa categoría. Aunque gran parte de esto originalmente venía de intentar acomodar arquitecturas de CPU antiguas sin perder rendimiento, así que llamarlo una “decisión de diseño” es un poco dudoso, casi como decir que las ruedas son redondas por una decisión de diseño
Hubo varios “escándalos” bastante conocidos cuando, alrededor de GCC 3.2, los compiladores empezaron a explotar el comportamiento indefinido de manera mucho más agresiva en las optimizaciones, y por eso mucha gente se quedó por bastante tiempo en GCC 2.95. GCC 3.2 salió en 2002
Como todas las empresas siguen enfatizando la seguridad y la exposición, es decir, salir en las noticias, la narrativa contra lo “inseguro” se volvió desproporcionadamente grande
El nuevo mundo se parece a gente de ciudad que nunca ha visto naturaleza de verdad y se asusta al ver una podadora. ¿Qué? ¿Tiene cuchillas girando? ¡No puede ser!
Si tu objetivo real es un pequeño sistema embebido encima de una torre de comunicaciones en medio de la nada, “funciona en mi máquina” no sirve de nada. Claro, la mayoría no trabaja en eso, y aquí probablemente la mayoría de los desarrolladores sean web developers, pero sigue siendo una discusión interesante aunque no lo hayas vivido directamente. De hecho, quizá por eso mismo
Un compilador puede tener bugs donde algo debería funcionar según la especificación y no lo hace, también hay muchas extensiones sin equivalente en el estándar, y hay comportamientos que el estándar deja indefinidos pero que en una implementación concreta reciben un resultado útil
En general estoy de acuerdo con la introducción, pero los ejemplos son malos y todo el artículo parece estar envuelto para empujar el coding con LLM
Da la impresión de alguien que quiere escribir cualquier código de cualquier manera y que funcione igual en todos los entornos. Si hicieras un lenguaje así, perderías la ventaja de poder escribir ajustado a la plataforma cuando quieras
El código C++ del artículo en parte no era idiomático desde hace más de 10 años, y hoy podría considerarse code smell
El lenguaje evolucionó bastante y ya no es el mismo que era cuando nació. En cuanto vi tantos raw pointers y tanto acceso directo por punteros, quedó claro que había que tomar parte del texto con pinzas
Otro problema evidente es la perspectiva de meter C y C++ en la misma bolsa como si fueran casi el mismo lenguaje. Hoy en día en realidad ya están bastante separados
std::atomicy noatomic_int¿Es correcto entender así el comportamiento indefinido en C?
Un programa P tiene un conjunto de entradas A que no provocan comportamiento indefinido, y un conjunto complementario B que sí lo provoca
Un compilador correcto compila P a un ejecutable P'. Para toda entrada de A, P' debe comportarse igual que P
Pero para cualquier entrada de B, no hay ningún requisito sobre el comportamiento de P'
Un ejemplo concreto de comportamiento indefinido causado por punteros desalineados: https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on...