c++ - sindicales - frases de sindicatos de trabajadores
Sindicatos y juegos de palabras (5)
El propósito original de Unions era ahorrar espacio cuando desea poder representar diferentes tipos, lo que llamamos un tipo de variante, vea Boost.Variant como un buen ejemplo de esto.
El otro uso común es el tipo de juego de palabras, se debate la validez de esto, pero prácticamente la mayoría de los compiladores lo soportan, podemos ver que gcc documenta su soporte :
La práctica de leer de un miembro del sindicato diferente al que se escribió más recientemente (llamado "tipo-juego de palabras") es común. Incluso con un alias de restricción, se permite la escritura de tipo, 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 -flexible-aliasing, type-punning está permitido lo que indica que hay un problema de aliasing 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 miembro utilizado por última vez 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 llamado "tipo de juego de palabras"). Esto podría ser una representación de trampa.
en C11 eso sería la nota al pie 95
.
Aunque en el tema del grupo de correo de std-discussion
estándar Tipo Punning a través de una Unión se hace el argumento, esto no está especificado, lo que parece razonable ya que el DR 283
no agregó una nueva redacción normativa, solo una nota al pie:
Esto es, en mi opinión, un atolladero semántico no especificado en C. No se ha llegado a un consenso entre los implementadores y el comité C sobre exactamente qué casos han definido un comportamiento y cuáles no [...]
En C ++ no está claro si se define el comportamiento o no .
Esta discusión también cubre al menos una razón por la cual no es deseable permitir el tipo de juego de palabras a través de una unión:
[...] las reglas del estándar C rompen las optimizaciones de análisis de alias basadas en el tipo que realizan las implementaciones actuales.
rompe algunas optimizaciones. El segundo argumento en contra de esto es que el uso de memcpy debe generar código idéntico y no rompe las optimizaciones y el comportamiento bien definido, por ejemplo esto:
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 usando godbolt, esto genera código idéntico y el argumento se hace si tu compilador no genera código idéntico, se debe considerar un error:
Si esto es cierto para su implementación, le sugiero que presente un error al respecto. Romper optimizaciones reales (cualquier cosa basada en el análisis de alias basado en tipo) para solucionar problemas de rendimiento con un compilador particular me parece una mala idea.
La publicación del blog Tipo Punning, Alias estricto y Optimización también llega a una conclusión similar.
La discusión indefinida de la lista de correos de comportamiento: escribir un juego de palabras para evitar copiar cubre gran parte del mismo terreno y podemos ver cuán gris puede ser el territorio.
Estuve buscando por un tiempo, pero no puedo encontrar una respuesta clara.
Mucha gente dice que usar uniones para escribir-juego de palabras es indefinido y una mala práctica. ¿Por qué es esto? No veo ninguna razón por la que haría algo indefinido, teniendo en cuenta que la memoria a la que escribes la información original no va a 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 regla de aliasing estricta, pero me parece que es como decir que no puedes hacerlo porque no puedes hacerlo.
Además, ¿qué sentido tiene una unión si no escribir un juego de palabras? Vi en alguna parte que se supone que deben usarse para usar la misma ubicación de memoria para diferentes informaciones en diferentes momentos, pero ¿por qué no simplemente eliminar la información antes de volver a usarla?
Resumir:
- ¿Por qué es malo usar sindicatos para el tipo de juego de palabras?
- ¿Qué sentido tienen si no es así?
Información adicional: estoy usando principalmente C ++, pero me gustaría saber acerca de eso y C. Específicamente estoy usando uniones para convertir entre flotadores y el hex bruto para enviar a través del bus CAN.
Es legal en C99:
Del estándar: 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 miembro utilizado por última vez 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 llamado "tipo de juego de palabras"). Esto podría ser una representación de trampa.
Hay (o al menos hubo, de vuelta en C90) dos modificaciones para hacer este comportamiento indefinido. La primera era que un compilador podría generar código adicional que rastreara lo que estaba en la unión, y generó una señal cuando accedió al miembro equivocado. En la práctica, no creo que nadie lo haya hecho nunca (¿tal vez CenterLine?). La otra fueron las posibilidades de optimización que esto abrió, y estas se usan. He utilizado compiladores que pospondrían una escritura hasta el último momento posible, sobre la base de que podría no ser necesario (porque la variable queda fuera 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 estaba en las primeras versiones de Microsoft C.
Los problemas del tipo de juego de palabras son complejos. El comité C (a fines de la década de 1980) adoptó más o menos la posición de que debería usar moldes (en C ++, reinterpretar_cast) para esto, y no uniones, aunque ambas técnicas se difundieron en ese momento. Desde entonces, algunos compiladores (g ++, por ejemplo) han tomado el punto de vista opuesto, apoyando el uso de uniones, pero no el uso de moldes. Y en la práctica, tampoco funciona si no es inmediatamente obvio que hay un tipo de juego de palabras. Esta podría ser la motivación detrás del punto de vista de g ++. Si accede a un miembro de la unión, es inmediatamente obvio que podría haber un tipo de 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 supone que pi
y pd
no pueden alias, y reordena la escritura a *pd
y la lectura de *pi
. (Creo que nunca fue la intención que esto esté garantizado. Pero la redacción actual de la norma lo garantiza).
EDITAR:
Dado que otras respuestas han argumentado que el comportamiento de hecho está definido (en gran medida se basa 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 tipo de juego de palabras. La razón probable de esto es que no hay un 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 definieran algo (y la mayoría, si no todas, lo hicieran).
Con respecto al uso de un sindicato para el tipo de juego: 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 realizaban comprobaciones adicionales (como el uso de indicadores de grasa para la comprobación de límites). De las discusiones en ese momento, estaba claro que la intención era que una implementación de depuración pudiera almacenar información sobre el último valor inicializado en una unión, y atrapar 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". El acceso a un valor que no está allí es un comportamiento indefinido; se puede asimilar para acceder a una variable no inicializada. (Hubo algunas discusiones en el momento sobre si el acceso a un miembro diferente con el mismo tipo era legal o no. No sé cuál fue la resolución final, sin embargo, después de alrededor de 1990, pasé a C ++).
Con respecto a la cita de C89, diciendo 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 de C90 en casa; el hecho de que haya sido eliminado en versiones posteriores de las normas sugiere que el comité consideró que su presencia era un error.
El uso de uniones que el estándar admite es 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 haya inicializado a través de inner
. (El hecho de que §6.5.2.3 / 6 comienza con "Se hace una garantía especial ..." y continúa permitiendo explícitamente que esto sea una indicación muy fuerte de que todos los demás casos están destinados a ser un comportamiento indefinido. Y, por supuesto, hay es la afirmación de que "el comportamiento indefinido está indicado en este Estándar internacional por las palabras '''' comportamiento indefinido '''' o por la omisión de cualquier definición explícita de comportamiento " en §2 / 2, para argumentar que el comportamiento no está indefinido , debe mostrar dónde se define en el estándar.)
Finalmente, con respecto al tipo de juego de palabras: todas las implementaciones (o al menos todas las que he usado) lo soportan de alguna manera. Mi impresión en ese momento era que la intención era que el lanzamiento del puntero fuera la forma en que una implementación lo apoyaba; en el estándar de C ++, existe un texto par (no normativo) para sugerir que los resultados de una reinterpret_cast
sean "sorprendentes" para alguien familiarizado con la arquitectura subyacente. En la práctica, sin embargo, la mayoría de las implementaciones admiten el uso de la unión para el tipo de juego 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 lanzamientos de punteros, siempre que el lanzamiento del 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 aliasing es invisible para el compilador. (Estoy bastante seguro de que esto es un defecto en el estándar. Me parece recordar incluso haber visto un DR relacionado).
Para volver a iterar, el tipo-juego de palabras a través de uniones está perfectamente bien en C (pero no en C ++). En contraste, el uso de moldes de puntero para hacerlo viola el alias estricto de C99 y es problemático porque los diferentes tipos pueden tener diferentes requisitos de alineación y usted puede generar un SIGBUS si lo hace incorrectamente. 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 ha 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 de postfix seguida de. operador y un identificador designa un miembro de una estructura o un objeto de unión. El valor es el del miembro nombrado
con la siguiente nota al pie 95:
Si el miembro utilizado para leer el contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez 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 llamado '''' tipo de juego de palabras ''''). Esto podría ser una representación de trampa.
Esto debería quedar perfectamente claro.
James está confundido porque C11 sección 6.7.2.1 §16 lee
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 mediante una expresión de tipo incompatible.
Ver también C11 anexo J.1 §1:
Los valores de bytes que corresponden a miembros de unión distintos de la última almacenada en [no están especificados].
En C99, esto solía leer
El valor de un miembro de la unión que no sea el ú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 próxima revisión estándar para ser reparado.
Las extensiones de GNU a C ++ estándar (y a C90) permiten explícitamente el tipo de juego de palabras con uniones . Otros compiladores que no son compatibles con las extensiones de GNU también pueden admitir el tipo de juego de unión, pero no es parte del estándar del lenguaje base.
BREVE RESPUESTA: El tipo de juego de palabras puede ser seguro en algunas circunstancias. Por otro lado, aunque parece ser una práctica muy conocida, parece que el estándar no está muy interesado en hacerlo oficial.
Hablaré solo de C (no de C ++).
1. TIPO PUNNING y LAS NORMAS
Como la gente ya señaló, el tipo de juego de palabras está permitido en el estándar C99 y también en C11, en la subsección 6.5.2.3 . Sin embargo, reescribiré los hechos con mi propia percepción del problema:
- La sección 6.5 de los documentos estándar C99 y C11 desarrolla el tema de las expresiones .
- La subsección 6.5.2 se refiere a las expresiones de postfix .
- La subsubsección 6.5.2.3 habla de estructuras y uniones .
- El párrafo 6.5.2.3 (3) explica el operador de punto aplicado a una
struct
o objeto deunion
, y qué valor se obtendrá.
Justo allí, aparece la nota al pie 95 . Esta nota al pie dice:
Si el miembro utilizado para acceder al contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez 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 llamado "tipo de juego de palabras"). Esto podría ser una representación de trampa.
El hecho de que el tipo de juego de palabras apenas aparece, y como nota al pie, da una pista de que no es un tema relevante en la programación C.
En realidad, el objetivo principal de usar unions
es ahorrar espacio (en la memoria). Dado que varios miembros comparten la misma dirección, si se sabe que cada miembro se utilizará en diferentes partes del programa, nunca al mismo tiempo, se puede usar una union
lugar de una struct
para guardar la memoria.
- La subsección 6.2.6 es mencionada.
- La subsección 6.2.6 habla de cómo se representan los objetos (en memoria, por ejemplo).
2. REPRESENTACIÓN DE TIPOS Y SU PROBLEMA
Si prestas atención a los diferentes aspectos del estándar, puedes estar seguro de casi nada:
- La representación de punteros no está claramente especificada.
- Lo peor, los punteros que tienen diferentes tipos podrían tener una representación diferente (como objetos en la memoria).
-
union
miembros delunion
comparten la misma dirección de encabezado en la memoria, y es la misma dirección que el objeto deunion
sí. -
struct
miembrosstruct
tienen dirección relativa creciente, comenzando exactamente en la misma dirección de memoria que el objetostruct
. Sin embargo, los bytes de relleno se pueden agregar al final de cada miembro. ¿Cuántos? Es impredecible Los bytes de relleno se usan principalmente para fines de alineación de memoria. - Los tipos aritméticos (números enteros, números reales y complejos en coma flotante) podrían ser representables de varias maneras. Depende de la implementación.
- En particular, los tipos enteros podrían tener bits de relleno . Esto no es verdad, creo, para computadoras de escritorio. Sin embargo, el estándar dejó la puerta abierta para esta posibilidad. Los bits de relleno se usan con fines especiales (paridad, señales, quién sabe) y no para contener valores matemáticos.
-
signed
tipossigned
pueden tener 3 maneras de ser representados: complemento de 1, complemento de 2, solo bit de signo. - Los tipos de caracteres ocupan solo 1 byte, pero 1 byte puede tener un número de bits diferente de 8 (pero nunca menos de 8).
Sin embargo, podemos estar seguros de algunos detalles:
a. Los tipos de caracteres no tienen bits de relleno.
segundo. Los tipos enterosunsigned
signo se representan exactamente como en forma binaria.
do.unsigned char
ocupa exactamente 1 byte, sin relleno de bits, y no hay ninguna representación de captura porque se utilizan todos los bits. Además, representa un valor sin ninguna ambigüedad, siguiendo el formato binario para números enteros.
3. TIPO PUNNING vs TYPE REPRESENTATION
Todas estas observaciones revelan que, si tratamos de hacer un tipo de juego de palabras con miembros de union
con tipos diferentes de caracteres unsigned char
, podríamos tener mucha ambigüedad. No es un código portátil y, en particular, podríamos tener un comportamiento irrecuperable de nuestro programa.
Sin embargo, el estándar permite este tipo de acceso .
Incluso si estamos seguros de la manera específica en que cada tipo se representa en nuestra implementación, podríamos tener una secuencia de bits que no significa nada en absoluto en otros tipos ( representación de trampas ). No podemos hacer nada en este caso.
4. EL CASO SEGURO: char sin firmar
La única manera segura de utilizar el tipo de juego de palabras es con unsigned char
o arreglos con unsigned char
(porque sabemos que los miembros de los objetos de la matriz son estrictamente contiguos y no hay bytes de relleno cuando su tamaño se calcula con sizeof()
).
union {
TYPE data;
unsigned char type_punning[sizeof(TYPE)];
} xx;
Como sabemos que el unsigned char
está representado en forma binaria estricta, sin bits de relleno, el tipo de juego de palabras se puede usar aquí para ver la representación binaria de los data
miembro.
Esta herramienta se puede usar para analizar cómo se representan los valores de un tipo dado, en una implementación particular.
No puedo ver otra aplicación segura y útil de tipo de juego de palabras bajo las especificaciones estándar.
5. UN COMENTARIO SOBRE CASTS ...
Si uno quiere jugar con tipos, es mejor definir sus propias funciones de transformación, o simplemente usar moldes . Podemos recordar este simple ejemplo:
union {
unsigned char x;
double t;
} uu;
bool result;
uu.x = 7;
(uu.t == 7.0)? result = true: result = false;
// You can bet that result == false
uu.t = (double)(uu.x);
(uu.t == 7.0)? result = true: result = false;
// result == true