c language-lawyer c99 c89

Programas válidos en C89, pero no en C99



language-lawyer (1)

¿Hay características / semánticas introducidas, o eliminadas, en C99 que podrían hacer un programa bien definido escrito en C89?

  • No válido (es decir, no compilar más, según el estándar C99)
  • Compilando, pero teniendo diferentes semánticas.

Mis hallazgos hasta ahora, con respecto a programas claramente inválidos:

  • int implícito (C89 §3.5.2)
  • Declaración de función implícita (C89 §3.3.2.2)
  • no regresa de una función que espera un valor de retorno (C89 §3.6.6.4)
  • utilizar nuevas palabras clave como identificador (por ejemplo, restrict , en inline , etc.)
  • hacks que implican // , que ahora son tratados como comentarios. Sin embargo, casi nunca se encuentra en el código de producción.

Cambios sutiles, haciendo que el mismo código tenga diferentes semánticas:

  • La división de enteros se ha definido bien, por ejemplo, -3 / 2 ahora tiene que truncarse hacia cero (C99 §6.5.5 / 6), en lugar de estar definido por la implementación (C89 §3.3.5 / 6)
  • strtod obtuvo la capacidad de analizar números hexadecimales en C99, al analizar 0x o 0X

¿Qué me he perdido?


Hay muchos programas que se hubieran considerado válidos en C89, antes de la publicación de C99, que algunas personas insisten en que nunca fueron válidos. C89 incluye una regla que requiere que solo se pueda acceder a un objeto de cualquier tipo utilizando un puntero de ese tipo, un tipo relacionado o un tipo de carácter. Antes de la publicación de C99, esta regla generalmente se interpretaba como aplicable solo a objetos "nombrados" (variables de duración estática o automática a las que se accede directamente por nombre), y solo en situaciones donde el objeto en cuestión no tenía su dirección tomado inmediatamente antes de que se usara como un tipo de puntero diferente. Dicha interpretación fue motivada por una serie de factores:

  1. Uno de los objetivos establecidos de la Norma era adaptarse a lo que los compiladores y programas existentes estaban haciendo, y si bien era raro que los programas existentes tuvieran acceso a variables con nombre discretas utilizando indicadores de diferentes tipos, excepto en los casos en que se tomó la dirección de la variable Inmediatamente antes de tal uso, muchos otros usos de punteros de tipo puntero eran bastante comunes.

  2. El fundamento del Estándar incluye como único ejemplo una función que recibe un puntero de un tipo primitivo para escribir una variable global de otro tipo primitivo de tal manera que un compilador no tenga una razón particular para esperar un aliasing. Ser capaz de mantener las variables globales en los registros es claramente una optimización útil, y el propósito establecido de la regla es permitir tales optimizaciones en los casos en que un compilador no tenga motivos para esperar que ocurra un aliasing. Prohibiendo construcciones como like (int*)&foo=23; no hace nada para ayudar a tales optimizaciones, ya que el hecho de que el código tome la dirección de foo y la anule la referencia debería dejarlo perfectamente claro para cualquier compilador que no esté deliberadamente obteniendo que el código va a modificar foo .

  3. Hay muchos tipos de código que requieren semánticamente la capacidad de usar bits de memoria como varios tipos, y nada en el Estándar indica que las reglas tenían la intención de hacer que los programadores salten a través de aros (por ejemplo, utilizando memcpy) para lograr una semántica que podría haber sido fácil obtenido en ausencia de las reglas, especialmente considerando que el uso de memcpy evitaría que el compilador mantenga las variables globales en los registros a través de los accesos de puntero ( anulando así el propósito para el cual se escribieron las reglas en primer lugar) .

  4. Si los tipos de estructura V y W tienen una secuencia inicial común, U es cualquier tipo de unión que contenga ambos, y p es un V* que identifica el V dentro de una U , entonces (W*)(U*)p puede usarse para acceder a esos miembros comunes, y será equivalente a (W*)p . A menos que un compilador pueda mostrar que p no podría ser un indicador de un miembro de alguna unión que contenga W , sería necesario permitir que (W*)p acceda a los miembros comunes; fue más útil simplemente tratar el acceso de los miembros comunes como legítimo, independientemente de si existe o dónde podría existir U , que buscar excusas para negarlo.

  5. No hay nada en las reglas de C89 que aclare cómo se define el "tipo" de una región de almacenamiento asignado, o cómo el almacenamiento que contiene cosas de un tipo que ya no son necesarias podría ser re-propuesto para mantener cosas de otro.

  6. El seguimiento de los registros asignados a las variables nombradas fue más fácil que el seguimiento de los registros asignados a otras excepciones de punteros, y el código que estaba interesado en minimizar el número de cargas y almacenes a través de los punteros a menudo copiaba las cosas a las variables nombradas y trabajaba allí.

C99 agregó reglas de "tipo efectivo" que son explícitamente aplicables al almacenamiento asignado. Algunas personas insisten en que esas fueron meramente "aclaraciones" de las reglas que ya existían en el C89, pero por las razones anteriores encuentro ese punto de vista insostenible. Está de moda afirmar que las únicas razones por las que los compiladores no aplicaron las reglas de alias a los objetos sin nombre son # 5 y # 6, pero las objeciones # 1- # 4 son igualmente significativas (y continúan aplicándose a C99 tanto como C89). Aún así, como C99 agregó las reglas de tipo efectivas, muchas construcciones que habrían sido tratadas como legítimas por la mayoría de las interpretaciones comunes de las reglas de C89 están claramente prohibidas.