oit - noticias de sindicatos
¿Es este uso de sindicatos estrictamente conforme? (4)
Con una interpretación estricta de la norma, este código podría no estar en conformidad . Centrémonos en el texto del conocido §6.5p7 :
Un objeto tendrá acceso a su valor almacenado solo por una expresión de valor l que tenga uno de los siguientes tipos:
- un tipo compatible con el tipo efectivo del objeto,
- una versión calificada de un tipo compatible con el tipo efectivo del objeto,
- un tipo que es el tipo con signo o sin signo correspondiente al tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subagregado o unión contenida), o
- Un tipo de personaje.
(énfasis mío)
Sus funciones read_s1x()
y write_s2x()
hacen lo contrario de lo que he marcado en negrita en el contexto de su código completo . Con solo este párrafo, podría concluir que no está permitido: un puntero a la union s1s2
podría alias un puntero a struct s1
, pero no al revés.
Por supuesto, esta interpretación significaría que el código debe funcionar según lo previsto si "en línea" estas funciones manualmente en su test()
. Este es realmente el caso aquí con gcc 6.2 para i686-w64-mingw32
.
Añadiendo dos argumentos a favor de la estricta interpretación presentada anteriormente:
Si bien siempre se permite un alias de cualquier puntero con
char *
, cualquier tipo de alias no puede utilizar un alias en la matriz de caracteres.Teniendo en cuenta el (aquí no relacionado) §6.5.2.3p6 :
Se otorga una garantía especial para simplificar el uso de uniones: si una unión contiene varias estructuras que comparten una secuencia inicial común (ver más abajo), y si el objeto de la unión actualmente contiene una de estas estructuras, se le permite inspeccionar la estructura común. Parte inicial de cualquiera de ellos en cualquier lugar que sea visible una declaración del tipo completado de la unión.
(de nuevo el énfasis es mío): la interpretación típica es que ser visible significa directamente en el alcance de la función en cuestión, no "en algún lugar de la unidad de traducción" ... así que esta garantía no incluye una función que lleve un puntero a Una de las
struct
que es miembro de launion
.
Dado el código:
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };
static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}
int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
if (read_s1x(&p1->v1))
{
unsigned short temp;
temp = p3->v1.x;
p3->v2.x = temp;
write_s2x(&p2->v2,1234);
temp = p3->v2.x;
p3->v1.x = temp;
}
return read_s1x(&p1->v1);
}
int test2(int x)
{
union s1s2 q[2];
q->v1.x = 4321;
return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
printf("%d/n",test2(0));
}
Existe un objeto de unión en todo el programa: q
. Su miembro activo se establece en v1
, y luego en v2
, y luego en v1
nuevamente. El código solo usa la dirección de operador en q.v1
, o el puntero resultante, cuando ese miembro está activo, e igualmente q.v2
. Como p1
, p2
y p3
son todos del mismo tipo, debería ser perfectamente legal usar p3->v1
para acceder a p1->v1
, y p3->v2
para acceder a p2->v2
.
No veo nada que pueda justificar que un compilador no genere la salida 1234, pero muchos compiladores, incluyendo clang y gcc, generan un código que genera 4321. Creo que lo que está sucediendo es que deciden que las operaciones en p3 no cambiarán realmente el contenido. De cualquier bit en la memoria, pueden ignorarse por completo, pero no veo nada en el Estándar que justifique ignorar el hecho de que p3
se usa para copiar datos de p1->v1
a p2->v2
y viceversa.
¿Hay algo en la Norma que justifique tal comportamiento, o los compiladores simplemente no lo siguen?
Creo que su código es conforme, y hay una falla con el -fstrict-aliasing
de GCC y Clang.
No puedo encontrar la parte correcta del estándar C, pero el mismo problema ocurre al compilar su código en el modo C ++ para mí, y encontré los pasajes relevantes del estándar C ++.
En el estándar de C ++, [class.union] / 5 define lo que sucede cuando se usa operator =
en una expresión de acceso de unión. El estándar de C ++ indica que cuando una unión está involucrada en la expresión de acceso de miembro del operador integrado =
, el miembro activo de la unión se cambia al miembro involucrado en la expresión (si el tipo tiene un constructor trivial, pero debido a esto es el código C, tiene un constructor trivial).
Tenga en cuenta que write_s2x
no puede cambiar el miembro activo de la unión, porque una unión no está involucrada en la expresión de asignación. Su código no asume que esto sucede, por lo que está bien.
Incluso si uso la ubicación new
para cambiar explícitamente qué miembro de la unión está activo, lo que debería ser una sugerencia para el compilador de que el miembro activo cambió, GCC aún genera un código que genera 4321
.
Esto parece un error con GCC y Clang suponiendo que el cambio de miembro de la unión activa no puede ocurrir aquí, porque no reconocen la posibilidad de que p1
, p2
y p3
apunten al mismo objeto.
GCC y Clang (y casi todos los demás compiladores) admiten una extensión a C / C ++ donde puedes leer a un miembro inactivo de una unión (obteniendo cualquier valor de basura potencial como resultado), pero solo si haces este acceso en un acceso de miembro Expresión que implica la unión. Si v1
no fuera el miembro activo, read_s1x
no se definiría el comportamiento bajo esta regla específica de la implementación, porque la unión no está dentro de la expresión de acceso del miembro. Pero como v1
es el miembro activo, eso no debería importar.
Este es un caso complicado, y espero que mi análisis sea correcto, como alguien que no es un mantenedor de compiladores o miembro de uno de los comités.
No leí el estándar, pero jugar con punteros en un modo de alias estricto (es decir, usar -fstrict-alising
) es peligroso. Ver el documento en línea de gcc :
Preste especial atención al código como este:
union a_union {
int i;
double d;
};
int f() {
union a_union t;
t.d = 3.0;
return t.i;
}
La práctica de leer de un miembro de la unión diferente a la que se escribió más recientemente (llamada
type-punning
) es común. Incluso con-fstrict-aliasing
, se permite la-fstrict-aliasing
tipos, siempre que se acceda a la memoria a través del tipo de unión. Por lo tanto, el código anterior funciona como se esperaba. Ver enumeración de uniones estructurales e implementación de campos de bits. Sin embargo, este código podría no:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
De manera similar, el acceso al tomar la dirección, lanzar el puntero resultante y eliminar la referencia al resultado tiene un comportamiento indefinido, incluso si la conversión utiliza un tipo de unión, por ejemplo:
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
La opción
-fstrict-aliasing
está habilitada en los niveles -O2, -O3, -Os.
Encontré algo similar en el segundo ejemplo, ¿eh?
No se trata de conformar o no conformar, es una de las "trampas" de optimización. Todas sus estructuras de datos se han optimizado y pasa el mismo puntero a los datos optimizados para que el árbol de ejecución se reduzca a una impresión simple del valor.
sub rsp, 8
mov esi, 4321
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
para cambiarlo, necesita hacer que esta función de "transferencia" sea propensa a los efectos secundarios y forzar las asignaciones reales. Forzará al optimizador a no reducir esos nodos en el árbol de ejecución:
int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/
main:
sub rsp, 8
mov esi, 1234
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
Es una prueba bastante trivial, hecha artificialmente un poco más complicada.