Nunca me di cuenta de que fflush (stdin) estaba mal hasta que me falló. ¿Qué otras cosas de uso común en C y C ++ generalmente funcionarán, pero en realidad se basan en un comportamiento indefinido?

Es común que los ingenieros mezclen el lenguaje C y la biblioteca estándar de C.

Los comportamientos indefinidos / no especificados se aplican al lenguaje en sí, y están bien descritos en la especificación del lenguaje C. (Todas las respuestas hasta ahora mencionan este tipo de comportamientos) Las funciones como fflush () se implementan en la biblioteca estándar C opcional, de una manera completamente específica de plataforma y compilador, o no se implementan en absoluto. Estas funciones NO son elementos del lenguaje, el comportamiento indefinido / no especificado no es aplicable a ellos. Son implementaciones de funciones reales, por lo que solo podemos hablar sobre el comportamiento específico de la implementación.

Las funciones de la biblioteca estándar de C generalmente se crean en lenguaje C. Cuando se ejecuta sobre un sistema operativo, la mayoría de las funciones están directamente conectadas a las funciones API del sistema operativo. (por ejemplo, manejo de archivos) Las peculiaridades del sistema operativo subyacente pueden estar expuestas a las aplicaciones que se ejecutan sobre él. Cuando se ejecuta sin sistema operativo, p. Ej., Un sistema MCU incrustado, el manejo de archivos rara vez se usa. Se puede implementar en la aplicación / controladores, o el entorno de depuración puede implementar al menos una salida estándar para transferir la transmisión a través de JTAG a la computadora host y mostrarla en una ventana de consola. Todas las implementaciones son fundamentalmente diferentes con limitaciones inherentes y específicas de la implementación y diferencias de comportamiento.

En una cierta compañía de biotecnología hace mucho tiempo y lejos, desarrollamos decenas de miles de líneas de código de control de robots usando Windows NT y Microsoft Visual Studio 5. Visual Studio permitió un modo de “depuración” que hizo varias cosas amables como inicializar todas las variables locales a cero. Por alguna razón, justo antes de que enviáramos una decisión, se redujo a Mejorar el rendimiento (¿fue malo?) Al cambiar al modo de optimización. Esto salvó literalmente docenas de ciclos de CPU y causó que explotaran acres de código y goteara por todo el piso. Después de una semana de perseguir errores de inicialización, la administración se dio por vencida y enviamos la versión de depuración.

Las variables locales de forma predeterminada no se inicializan, es decir, se inicializan en basura aleatoria en la memoria. Esto puede hacer lo que quieras la mayor parte del tiempo. No hará lo que quieras todo el tiempo.

Visto lo siguiente en código en vivo (código WCDMA y TDSCDMA para algunas plataformas de módem (no solo indefinido, y solo una pequeña muestra):

Usando a ^ = b ^ = a ^ = b; intercambiar valores, ignorando que la expresión modifica un objeto más de una vez entre puntos de secuencia.
Suponiendo que un campo de bits “normal” sea sin signo o firmado
Cambio por valores negativos (¡porque funcionó en gcc!)
Desplazar a la derecha un int con signo negativo siempre dará como resultado un desplazamiento aritmético.

Por supuesto, cuando podría decir que si funciona como se esperaba, entonces probablemente estemos bien (no es bueno cuando se trata de un comportamiento indefinido) nos divertimos mucho al cambiar compiladores porque algunos grandes clientes querían RVCT (no puedo recordar qué estaba en uso antes, ¿IAR quizás?).

(Ab) el uso de tipos de unión como sustituto de la reinterpretación de bits no está técnicamente definido por el estándar C, aunque estoy bastante seguro de que prácticamente ninguna implementación hará las cosas de una manera tan absurda que ya no “funcionará” .

Entonces, por ejemplo, si tomáramos la (in) famosa función Fast Inverse Square Root de Quake III Arena:

float Q_rsqrt (número flotante)
{
largo i;
flotador x2, y;
const flotante tres medias = 1.5F;

x2 = número * 0.5F;
y = número;
i = * (largo *) & y; // piratería de nivel de bits de punto flotante malvado
i = 0x5f3759df – (i >> 1); // que carajo?
y = * (flotante *) & i;
y = y * (tres medias – (x2 * y * y));

volver y;
}

… algunas personas habrían hecho lo mismo usando una unión, así:

typedef union evil_bit_hacking {
flotador y;
largo i;
} EBH;

float Q_rsqrt (número flotante)
{
flotador x2;
EBH maldad;
const float threehalfs = 1.5F;

x2 = número * 0.5F;
evil.y = número; // ¡ahora aún más malvado!
evil.i = 0x5f3759df – (evil.i >> 1); // que carajo?
evil.y = evil.y * (tres medias – (x2 * evil.y * evil.y));

devuelve el mal.
}

… con la advertencia, por supuesto, de que tal comportamiento es técnicamente indefinido / no especificado (solo se supone que debes leer el campo que se escribió por última vez). Pero por lo general funciona, y es difícil pensar en una situación en la que una implementación optaría por no hacer el “sentido común”, por lo que mucha gente todavía lo hace de todos modos.

Como mi fuente, aquí están las partes relevantes del estándar C:

6.2.6 Representaciones de tipos

6.2.6.1 General

6 Cuando un valor se almacena en un objeto de estructura o tipo de unión, incluso en un objeto miembro, los bytes de la representación del objeto que corresponden a los bytes de relleno toman valores no especificados.

7 Cuando un valor se almacena en un miembro de un objeto de tipo unión, los bytes de la representación del objeto que no corresponden a ese miembro pero sí corresponden a otros miembros toman valores no especificados.

Sin embargo, de cualquier manera, como dijo una vez mi profesor de Sistemas, “Los sindicatos son una forma maravillosa de plantar el pie directamente delante de usted, para que pueda apretar el gatillo”.

printf (“la cosa tiene el tamaño% d \ n”, sizeof (cosa));

size_t no es un int imprimible con% d, técnicamente, pero en estos días casi siempre funciona. Por lo general, puede enviarlo a int y no estar realmente equivocado, aunque técnicamente podría estar truncando un tamaño enorme; o puede usar uno de los tipos largos de sabor “% lld” y convertirlos en un tipo largo específico, pero estos son incómodos con su plataforma cruzada porque algunas bibliotecas tienen una sintaxis diferente para esos (% l64d ???), y eso correctamente es un dolor.

Esa es un área desordenada y las anteojeras reducen la incidencia de migraña.

El módulo (%) con enteros negativos depende técnicamente del compilador / máquina. Aquí hay un documento que habla sobre esto y cómo obtener resultados consistentes.

Y aquí hay un resumen de cómo los diferentes idiomas manejan el módulo con enteros negativos.