Sindicatos y juegos de azar


He estado buscando por un tiempo, pero no puedo encontrar una respuesta clara.

Mucha gente dice que usar uniones para escribir un juego de palabras es indefinido y una mala práctica. ¿Por qué es esto? No puedo ver ninguna razón por la que haría algo indefinido teniendo en cuenta que la memoria en 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 de unión, eso sería un mal diseño).

La gente cita la estricta regla de aliasing, pero me parece que es como decir que no puedes hacerlo porque no puedes hacerlo.

También, ¿cuál es el punto de una unión si no es escribir un juego de palabras? Vi en algún lugar 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 eliminar la información antes de volver a usarla?

Para resumir:

  1. ¿Por qué es malo usar uniones para el juego de palabras?
  2. ¿Cuál es el punto de ellos si no es esto?

Extra información: Estoy usando principalmente C++, pero me gustaría saber sobre eso y C. Específicamente estoy usando uniones para convertir entre flotadores y el hex raw para enviar a través del bus CAN.

Author: Shafik Yaghmour, 2014-09-04

5 answers

Para repetir, el juego de palabras a través de uniones está perfectamente bien en C (pero no en C++). Por el contrario, el uso de moldes de puntero para hacerlo viola el alias estricto de C99 y es problemático porque diferentes tipos pueden tener diferentes requisitos de alineación y 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 union después de el valor se ha almacenado 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 postfix seguida por el . operador y un identificador designa un miembro de una estructura u objeto de unión. El valor es el del miembro nombrado

Con la siguiente nota 95:

Si el miembro utilizado para leer el contenido de un objeto union 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 del objeto del valor se reinterpreta como una representación del objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "tipo punning"). Esto podría ser una representación trampa.

Esto debería estar perfectamente claro.


James está confundido porque C11 sección 6.7.2.1 §16 dice

El valor de como máximo uno de los miembros puede almacenarse en un objeto union en cualquier momento.

Esto parece contradictorio, pero no lo es: A diferencia de C++, en C, no hay 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 decir

El valor de un miembro del sindicato distinto del último almacenado en [es no especificado]

Esto era incorrecto. Como el anexo no es normativo, no calificó su propio CT y tuvo que esperar hasta la próxima revisión estándar para arreglarlo.


Las extensiones GNU para C++ estándar (y para C90) permiten explícitamente el juego de palabras con uniones. Otros compiladores que no soportan extensiones de GNU también pueden soportar union type-punning, pero no es parte del estándar del lenguaje base.

 32
Author: Christoph,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-20 05:19:14

El propósito original de Unions era ahorrar espacio cuando se quiere poder representar diferentes tipos, lo que llamamos un tipo de variante ver Boost.Variante como un buen ejemplo de esto.

El otro uso común es type punning la validez de esto se debate pero prácticamente la mayoría del compilador lo soporta, podemos ver que gcc documenta su soporte :

La práctica de leer de un miembro del sindicato diferente al más reciente escrito a (llamado "tipo-juego de palabras") es común. Incluso con-fstrict-aliasing, se permite el juego de palabras de tipo, siempre que se acceda a la memoria a través del tipo union. Por lo tanto, el código anterior funciona como se esperaba.

Tenga en cuenta que dice incluso con-fstrict-aliasing, se permite el juego de palabras lo que indica que hay un problema de aliasing en juego.

Pascal Cuoq ha argumentado que informe de defectos 283 aclaró que esto estaba permitido en C. Informe de defectos 283 agregó lo siguiente nota de pie de página como aclaración:

Si el miembro utilizado para acceder al contenido de un objeto union 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 del objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "tipo punning"). Esto podría ser una representación trampa.

En C11 eso sería una nota al pie 95.

Aunque en el std-discussion mail group topic Escriba el juego de palabras a través de una Unión el argumento se hace esto está poco especificado, lo que parece razonable ya que DR 283 no agregó una nueva redacción normativa, solo una nota al pie:

Esto es, en mi opinión, un atolladero semántico poco específico en C. No se ha llegado a un consenso entre los ejecutores y el C comité en cuanto a exactamente qué casos han definido el comportamiento y cuáles ni[...]

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 que permitir el juego de tipo a través de una unión es indeseable:

[...]las reglas del estándar C rompen el alias basado en tipos optimizaciones de análisis 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 su compilador no genera código idéntico debería considerarse un error:

Si esto es cierto para su implementación, le sugiero que presente un error en ella. Romper optimizaciones reales (cualquier cosa basada en el análisis de alias basado en tipos) para solucionar problemas de rendimiento con algún compilador en particular parece un mal idea para mí.

La entrada del blog Type Punning, Strict Aliasing, and Optimization también llega a una conclusión similar.

La discusión de la lista de correo de comportamiento indefinido: Escriba punning para evitar copiar cubre mucho del mismo terreno y podemos ver lo gris que puede ser el territorio.

 9
Author: Shafik Yaghmour,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:20

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 union no es el igual que el miembro utilizado por última vez para almacenar un valor en el objeto, el la parte apropiada de la representación del objeto del valor es reinterpretado como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "tipo de juego de palabras"). Esto podría ser un trampa representatividad.

 5
Author: Keine Lust,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-04 12:01:06

RESPUESTA BREVE: El juego de palabras puede ser seguro en algunas circunstancias. Por otro lado, aunque parece ser una práctica muy conocida, parece que la norma no está muy interesada en hacerla oficial.

Solo hablaré de C (no de C++).

1. TIPO JUEGO DE PALABRAS y LOS ESTÁNDARES

Como la gente ya señaló, pero, tipo de juego de palabras se permite en el estándar C99 y también C11, en la subsección 6.5.2.3. Sin embargo, voy a reescribir los hechos con mi propia percepción del tema:

  • La sección 6.5 de los documentos estándar C99 y C11 desarrollan el tema de expresiones.
  • La subsección 6.5.2 se refiere a expresiones postfix.
  • La subsección 6.5.2.3 habla de estructuras y sindicatos .
  • El párrafo 6.5.2.3(3) explica el operador punto aplicado a un objeto struct o union, y qué valor se obtendrá.
    Justo ahí aparece la[24]}nota 95 . Esta nota dice:

Si el miembro utilizado para acceder al contenido de un objeto union 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 del objeto del valor se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "tipo punning"). Esto podría ser una representación trampa.

El hecho de que escriba punning apenas aparece, y como nota al pie, da una pista de que no es un tema relevante en la programación en C.
En realidad, el propósito principal de usar unions es ahorrar espacio (en memoria). Dado que varios miembros comparten la misma dirección, si uno sabe que cada miembro utilizará diferentes partes del programa, nunca al mismo tiempo, entonces se puede usar un union en lugar de un struct, para guardar memoria.

  • La subsección 6.2.6 se menciona.
  • La subsección 6.2.6 habla de cómo se representan los objetos (en memoria, por ejemplo).

2. REPRESENTACIÓN DE TIPOS y SUS PROBLEMAS

Si prestas atención a los diferentes aspectos del estándar, puedes estar seguro de casi nada: {[28]]}

  • La representación de punteros no está claramente especificada.
  • Peor, los punteros tienen diferentes los tipos pueden tener una representación diferente (como objetos en memoria).
  • union los miembros comparten la misma dirección de encabezado en la memoria, y es la misma dirección que la del objeto union en sí.
  • struct los miembros tienen una dirección relativa creciente, comenzando exactamente en la misma dirección de memoria que el propio objeto struct. Sin embargo, se pueden agregar bytes de relleno al final de cada miembro. ¿Cuántos? Es impredecible. Los bytes de relleno se utilizan principalmente para fines de alineación de memoria.
  • Los tipos aritméticos (enteros, números reales de coma flotante y números complejos) podrían representarse de varias maneras. Depende de la implementación.
  • En particular, los tipos enteros podrían tener bits de relleno. Esto no es cierto, creo, para las computadoras de escritorio. Sin embargo, el estándar dejó la puerta abierta para esta posibilidad. Los bits de relleno se utilizan con fines específicos (paridad, señales, quién sabe), y no para mantener valores matemáticos.
  • signed tipos puede tener 3 maneras de ser representado: complemento de 1, complemento de 2, solo signo-bit.
  • Los tipos char ocupan solo 1 byte, pero 1 byte puede tener un número de bits diferentes de 8 (pero nunca menos de 8).
  • Sin embargo, podemos estar seguros de algunos detalles:

    A. Los tipos char no tienen bits de relleno.
    b. Los tipos enteros unsigned se representan exactamente como en forma binaria.
    c. unsigned char ocupa exactamente 1 byte, sin bits de relleno, y no hay ninguna representación trap 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 JUEGO DE PALABRAS vs TIPO REPRESENTACIÓN

Todas estas observaciones revelan que, si tratamos de hacer juegos de tipo con union miembros que tienen tipos diferentes de unsigned char, podríamos tener mucha ambigüedad. No es código portable y, en particular, podríamos tener un comportamiento umpredictable 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 está representado en nuestra implementación, podríamos tener una secuencia de bits que no significa nada en absoluto en otros tipos ( representación trap). No podemos hacer nada en este caso.

4. EL CASO SEGURO: unsigned char

La única forma segura de usar el tipo de juego es con unsigned char o bien unsigned char arrays (porque sabemos que los miembros de los objetos array 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;  

Dado que sabemos que unsigned char se representa en forma binaria estricta, sin bits de relleno, el tipo punning se puede usar aquí para echar un vistazo a la representación binaria del miembro data.
Esta herramienta se puede utilizar 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 punning bajo las especificaciones estándar.

5. UN COMENTARIO SOBRE LOS MOLDES...

Si uno quiere jugar con tipos, es mejor definir sus propias funciones de transformación, o bien simplemente usar casts. 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
 3
Author: pablo1977,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-04 14:43:08

Hay (o al menos hubo, en C90) dos modificaciones para haciendo este comportamiento indefinido. El primero fue que un compilador se le permitiría generar código adicional que rastreara lo que en la unión, y generó una señal cuando accedió al mal miembro. En la práctica, no creo que nadie lo haya hecho (tal vez ¿Línea central?). La otra era las posibilidades de optimización de este abierto, y estos se utilizan. He utilizado compiladores que aplazaría una escritura hasta el último momento posible, en el motivos que podrían no ser necesarios (porque la variable sale de alcance, o hay una escritura posterior de un diferente valor). Lógicamente, uno esperaría que esta optimización se apagaría cuando la unión fuera visible, pero no estaba en las primeras versiones de Microsoft C.

Los problemas del juego de tipo son complejos. El comité C (volver a finales de 1980) más o menos tomó la posición de que usted debería usar casts (en C++, reinterpret_cast) para esto, y ni sindicatos, aunque ambas técnicas estaban muy extendidas en ese momento. Desde entonces, algunos compiladores (g++, por ejemplo) han tomado el punto de vista opuesto, apoyando el uso de sindicatos, pero no el uso de moldes. Y en la práctica, ni funciona si no lo es inmediatamente obvio que hay tipo-juego de palabras. Esto podría ser la motivación detrás del punto de vista de g++. Si usted accede un miembro del sindicato, es inmediatamente obvio que podría haber juego de tipo. 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 de la estándar, pero falla con g++ (y probablemente muchos otros compiladores); al compilar f, el compilador asume que pi y pd no puede alias, y reordena la escritura a *pd y el leer de *pi. (Creo que nunca fue la intención que esto está garantizado. Pero la redacción actual de la norma lo garantiza.)

EDITAR:

Ya que otras respuestas tienen argumentó que el comportamiento es de hecho definido (basado en gran medida en citar una nota no normativa, tomada fuera de contexto):

La respuesta correcta aquí es la de pablo1977: el estándar hace ningún intento de definir el comportamiento de tipo descriptivo está involucrado. La razón probable de esto es que no hay portable comportamiento que podría definir. Esto no impide una implementación desde su definición; aunque no recuerdo ninguna discusiones específicas del tema, estoy bastante seguro que el la intención era que las implementaciones definan algo (y la mayoría, si no todos, hacer).

Con respecto al uso de una unión para el tipo-juego de palabras: cuando el C comité estaba desarrollando C90 (a finales de 1980), había una clara intención de permitir la depuración de implementaciones que comprobación adicional (como el uso de indicadores de grasa para límites comprobación). De las discusiones en el momento, estaba claro que el la intención era que una implementación de depuración pudiera almacenar en caché información relativa al último valor inicializado en una unión, y trampa si intentaste acceder a cualquier otra cosa. Esto es claramente establecido en §6.7.2.1/16: "El valor de como máximo uno de los miembros se puede almacenar en un objeto union en cualquier momento."Acceso a un valor que no hay un comportamiento indefinido; se puede asimilar a accediendo a una variable no inicializada. (Había algunos discusiones en el momento en cuanto a si el acceso a un diferente miembro con el mismo tipo era legal o no. No sé qué la resolución final fue, sin embargo; después de alrededor de 1990, seguí adelante a C++.)

Con respecto a la cita de C89, diciendo que el comportamiento es definición de implementación: encontrarlo en la sección 3 (Términos, Definiciones y Símbolos) parece muy extraño. Tendré que mirar en mi copia de C90 en casa; el hecho de que ha sido eliminado en versiones posteriores de los estándares sugiere que su el Comité consideró que la presencia era un error.

El uso de uniones que el estándar soporta es como un medio para simular derivación. Puede 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 acceso legal a la base.tipo, a pesar de que el Nodo era 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 explícitamente permitir esto es una indicación muy fuerte de que todos los demás los casos están destinados a ser un comportamiento indefinido. Y por supuesto, hay es la declaración de que "El comportamiento indefinido se indica de otra manera en este Estándar Internacional por las palabras " undefined comportamiento " 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 al juego de tipo: todo (o al menos todo eso He usado) las implementaciones lo soportan de alguna manera. Mi la impresión en ese momento era que la intención era que el puntero casting ser la forma en que una implementación lo soportó; en el C++ estándar, incluso hay texto (no normativo) para sugerir que el resultados de una reinterpret_cast sea "no sorprendente" para alguien familiarizado con la arquitectura subyacente. En la práctica, sin embargo, la mayoría de las implementaciones apoyan el uso de union para tipo-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 moldes de puntero, siempre que el puntero sea claramente visible para el compilador (para alguna definición no especificada de pointer cast). Y el "estandarización" del hardware subyacente significa que las cosas como:

int
getExponent( double d )
{
    return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}

Son en realidad bastante portable. (No funcionará en mainframes, de curso.) Lo que no funciona son cosas como mi primer ejemplo, donde el alias es invisible para el compilador. (I'm pretty seguro que esto es un defecto en el estándar. Me parece recordar incluso habiendo visto a un DR con respecto a ello.)

 2
Author: James Kanze,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-05 08:54:10