¿Está permitido utilizar uniones para juegos de palabras tipográficos y, de no ser así, por qué?
He estado buscando por un tiempo, pero no encuentro una respuesta clara.
Mucha gente dice que utilizar sindicatos para hacer juegos de palabras no está definido y es una mala práctica. ¿Por qué es esto? No veo ninguna razón por la que haría algo indefinido considerando que la memoria en la que escribe la información original no cambiará por sí sola (a menos que salga del alcance de la pila, pero eso no es un problema sindical). , eso sería un mal diseño).
La gente cita la estricta regla del alias, pero me parece como decir que no puedes hacerlo porque no puedes hacerlo.
Además, ¿cuál es el punto de una unión si no es un juego de palabras? Vi en alguna parte que se supone que deben usarse para usar la misma ubicación de memoria para información diferente en diferentes momentos, pero ¿por qué no simplemente eliminar la información antes de usarla nuevamente?
Resumir:
- ¿Por qué es malo utilizar sindicatos para juegos de palabras tipográficos?
- ¿Cuál es el punto de ellos si no es este?
Información adicional: estoy usando principalmente C++, pero me gustaría saber sobre eso y C. Específicamente estoy usando uniones para convertir entre flotantes y hexadecimal sin formato para enviar a través del bus CAN.
Para reiterar, los juegos de palabras a través de uniones están perfectamente bien en C (pero no en C++). Por el contrario, el uso de conversiones de punteros para hacerlo viola el estricto alias C99 y es problemático porque diferentes tipos pueden tener diferentes requisitos de alineación y usted podría generar un SIGBUS si lo hace mal. Con los sindicatos, esto nunca es un problema.
Las citas relevantes de los estándares C son:
C89 sección 3.3.2.3 §5:
Si se accede a un miembro de un objeto de unión después de que se haya almacenado un valor en un miembro diferente del objeto, el comportamiento está definido por la implementación.
C11 sección 6.5.2.3 §3:
Una expresión sufija seguida del . operador y un identificador designan un miembro de una estructura u objeto de unión. El valor es el del miembro nombrado.
con la siguiente nota a pie de página 95:
Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el último miembro utilizado para almacenar un valor en el objeto, la parte apropiada de la representación de objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como descrito en 6.2.6 (un proceso a veces denominado "juego de palabras tipo"). Esta podría ser una representación de trampa.
Esto debería quedar perfectamente claro.
James está confundido porque la sección C11 6.7.2.1 §16 dice
El valor de como máximo uno de los miembros se puede almacenar en un objeto de unión en cualquier momento.
Esto parece contradictorio, pero no lo es: a diferencia de C++, en C no existe el concepto de miembro activo y está perfectamente bien acceder al único valor almacenado a través de una expresión de un tipo incompatible.
Véase también C11 anexo J.1 §1:
Los valores de bytes que corresponden a miembros de la unión distintos del último almacenado en [no están especificados].
En C99, esto solía leer
El valor de un miembro del sindicato distinto del último almacenado en [no está especificado]
Esto fue incorrecto. Como el anexo no es normativo, no calificó su propio TC y tuvo que esperar hasta la siguiente revisión estándar para corregirlo.
Las extensiones de GNU al C++ estándar (y al C90) permiten explícitamente los juegos de palabras con uniones . Otros compiladores que no soportan extensiones GNU también pueden soportar el juego de palabras tipo unión, pero no es parte del estándar del lenguaje base.
El propósito original de las uniones era ahorrar espacio cuando se desea poder representar diferentes tipos; lo que llamamos un tipo variante , consulte Boost.Variant como un buen ejemplo de esto.
El otro uso común es el juego de palabras; se debate su validez, pero prácticamente la mayoría de los compiladores lo admiten, podemos ver que gcc documenta su soporte :
La práctica de leer de un miembro del sindicato diferente al de aquel a quien se le escribió más recientemente (lo que se denomina “juego de palabras”) es común. Incluso con -fstrict-aliasing, se permiten juegos de palabras, siempre que se acceda a la memoria a través del tipo de unión. Entonces, el código anterior funciona como se esperaba.
tenga en cuenta que dice que incluso con -fstrict-aliasing, se permiten juegos de palabras , lo que indica que hay un problema de alias en juego.
Pascal Cuoq ha argumentado que el informe de defectos 283 aclaró que esto estaba permitido en C. El informe de defectos 283 añadió la siguiente nota a pie de página como aclaración:
Si el miembro utilizado para acceder al contenido de un objeto de unión no es el mismo que el último miembro utilizado para almacenar un valor en el objeto, la parte apropiada de la representación de objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como descrito en 6.2.6 (un proceso a veces denominado "juego de palabras tipo"). Esta podría ser una representación de trampa.
en C11 eso sería una nota a pie de página 95
.
Aunque en el std-discussion
tema del grupo de correo Type Punning via a Union se argumenta que esto no está suficientemente especificado, lo que parece razonable ya que DR 283
no agregó nueva redacción normativa, solo una nota al pie:
Esto es, en mi opinión, un atolladero semántico poco especificado en C. No se ha llegado a un consenso entre los implementadores y el comité C sobre exactamente qué casos tienen un comportamiento definido y cuáles no[...]
En C++ no está claro si hay un comportamiento definido o no .
Esta discusión también cubre al menos una razón por la cual no es deseable permitir juegos de palabras tipográficos a través de una unión:
[...] las reglas del estándar C rompen las optimizaciones del análisis de alias basado en tipos que realizan las implementaciones actuales.
rompe algunas optimizaciones. El segundo argumento en contra de esto es que el uso de memcpy debería generar código idéntico y no interrumpe las optimizaciones ni el comportamiento bien definido, por ejemplo este:
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
en lugar de esto:
union u1
{
std::int64_t n;
double d ;
} ;
u1 u ;
u.d = d ;
y podemos ver que al usar godbolt esto genera un código idéntico y el argumento es que si su compilador no genera un código idéntico, debería considerarse un error:
Si esto es cierto para su implementación, le sugiero que presente un error. Romper optimizaciones reales (cualquier cosa basada en análisis de alias basado en tipos) para solucionar problemas de rendimiento con algún compilador en particular me parece una mala idea.
La publicación del blog Type Punning, Strict Aliasing y Optimization también llega a una conclusión similar.
La discusión sobre el comportamiento indefinido de la lista de correo: juegos de palabras tipográficos para evitar la copia cubre gran parte del mismo terreno y podemos ver cuán gris puede ser el territorio.
Hay (o al menos había, en C90) dos modificaciones para hacer que este comportamiento sea indefinido. La primera era que a un compilador se le permitiría generar código adicional que rastreaba lo que había en la unión y generaba una señal cuando se accedía al miembro equivocado. En la práctica, no creo que nadie lo haya hecho (¿tal vez CenterLine?). La otra fueron las posibilidades de optimización que esto abrió y que se utilizan. He utilizado compiladores que aplazarían una escritura hasta el último momento posible, con el argumento de que podría no ser necesario (porque la variable sale del alcance o hay una escritura posterior de un valor diferente). Lógicamente, uno esperaría que esta optimización se desactivara cuando la unión fuera visible, pero no era así en las primeras versiones de Microsoft C.
Las cuestiones relacionadas con los juegos de palabras tipográficos son complejas. El comité C (a finales de la década de 1980) adoptó más o menos la posición de que se debían utilizar conversiones (en C++, reinterpret_cast) para esto, y no uniones, aunque ambas técnicas estaban muy extendidas en ese momento. Desde entonces, algunos compiladores (g++, por ejemplo) han adoptado el punto de vista opuesto, apoyando el uso de uniones, pero no el uso de conversiones. Y en la práctica, ninguno de los dos funciona si no es inmediatamente obvio que se trata de un juego de palabras. Esta podría ser la motivación detrás del punto de vista de g++. Si accede a un miembro del sindicato, es inmediatamente obvio que puede haber un juego de palabras. Pero por supuesto, dado algo como:
int f(const int* pi, double* pd)
{
int results = *pi;
*pd = 3.14159;
return results;
}
llamado con:
union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );
es perfectamente legal según las estrictas reglas del estándar, pero falla con g++ (y probablemente con muchos otros compiladores); Al compilar f
, el compilador asume que pi
no pd
puede crear un alias y reordena la escritura *pd
y la lectura *pi
. (Creo que nunca fue la intención que esto se garantizara. Pero la redacción actual de la norma sí lo garantiza).
EDITAR:
Dado que otras respuestas han argumentado que el comportamiento de hecho está definido (basado en gran medida en citar una nota no normativa, sacada de contexto):
La respuesta correcta aquí es la de pablo1977: el estándar no intenta definir el comportamiento cuando se trata de juegos de palabras. La razón probable de esto es que no existe ningún comportamiento portátil que pueda definir. Esto no impide que una implementación específica lo defina; aunque no recuerdo ninguna discusión específica sobre el tema, estoy bastante seguro de que la intención era que las implementaciones definan algo (y la mayoría, si no todas, lo hacen).
Con respecto al uso de una unión para juegos de palabras: cuando el comité C estaba desarrollando C90 (a fines de la década de 1980), había una clara intención de permitir implementaciones de depuración que hicieran comprobaciones adicionales (como el uso de punteros gruesos para comprobar los límites). De las discusiones en ese momento, quedó claro que la intención era que una implementación de depuración pudiera almacenar en caché información sobre el último valor inicializado en una unión y capturar si intentaba acceder a cualquier otra cosa. Esto se establece claramente en §6.7.2.1/16: "El valor de como máximo uno de los miembros se puede almacenar en un objeto de unión en cualquier momento". Acceder a un valor que no existe es un comportamiento indefinido; se puede asimilar a acceder a una variable no inicializada. (Hubo algunas discusiones en ese momento sobre si acceder a un miembro diferente con el mismo tipo era legal o no. Sin embargo, no sé cuál fue la resolución final; aproximadamente después de 1990, pasé a C++).
Con respecto a la cita de C89, decir que el comportamiento está definido por la implementación: encontrarlo en la sección 3 (Términos, definiciones y símbolos) parece muy extraño. Tendré que buscarlo en mi copia del C90 que tengo en casa; el hecho de que haya sido eliminado en versiones posteriores de las normas sugiere que el comité consideró su presencia un error.
El uso de uniones que admite la norma es como un medio para simular la derivación. Puedes definir:
struct NodeBase
{
enum NodeType type;
};
struct InnerNode
{
enum NodeType type;
NodeBase* left;
NodeBase* right;
};
struct ConstantNode
{
enum NodeType type;
double value;
};
// ...
union Node
{
struct NodeBase base;
struct InnerNode inner;
struct ConstantNode constant;
// ...
};
y acceder legalmente a base.type, aunque el Nodo se inicializó mediante inner
. (El hecho de que §6.5.2.3/6 comience con "Se ofrece una garantía especial..." y continúe permitiendo esto explícitamente es una indicación muy fuerte de que todos los demás casos deben ser comportamientos indefinidos. Y, por supuesto, hay es la afirmación de que "el comportamiento indefinido se indica en esta Norma Internacional mediante las palabras ''comportamiento indefinido'' o por la omisión de cualquier definición explícita de comportamiento " en §4/2; con el fin de argumentar que el comportamiento no es indefinido. , debe mostrar dónde está definido en el estándar).
Finalmente, con respecto a los juegos de palabras: todas las implementaciones (o al menos todas las que he usado) lo admiten de alguna manera. Mi impresión en ese momento fue que la intención era que la conversión de punteros fuera la forma en que una implementación lo respaldara; en el estándar C++, incluso hay texto (no normativo) que sugiere que los resultados de a no reinterpret_cast
serán "sorprendentes" para alguien familiarizado con la arquitectura subyacente. En la práctica, sin embargo, la mayoría de las implementaciones admiten el uso de union para juegos de palabras, siempre que el acceso sea a través de un miembro del sindicato. La mayoría de las implementaciones (pero no g++) también admiten conversiones de puntero, siempre que la conversión de puntero sea claramente visible para el compilador (para alguna definición no especificada de conversión de puntero). Y la "estandarización" del hardware subyacente significa que cosas como:
int
getExponent( double d )
{
return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}
en realidad son bastante portátiles. (No funcionará en mainframes, por supuesto). Lo que no funciona son cosas como mi primer ejemplo, donde el alias es invisible para el compilador. (Estoy bastante seguro de que esto es un defecto en el estándar. Creo recordar incluso haber visto un DR al respecto).
Es legal en C99:
De la norma: 6.5.2.3 Estructura y miembros del sindicato
Si el miembro utilizado para acceder al contenido de un objeto de unión no es el mismo que el último miembro utilizado para almacenar un valor en el objeto, la parte apropiada de la representación de objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como descrito en 6.2.6 (un proceso a veces denominado "juego de palabras tipo"). Esta podría ser una representación de trampa.