variable - unions in c examples
¿Por qué no es válido que un tipo de unión declarado en una función se use en otra función? (3)
Aquí está la regla de aliasing estricta en acción: una suposición hecha por el compilador de C (o C ++), es que la desreferenciación de los punteros a objetos de diferentes tipos nunca se referirá a la misma ubicación de memoria (es decir, alias entre sí).
Esta función
int f(struct t1* p1, struct t2* p2);
asume que p1 != p2
porque formalmente apuntan a diferentes tipos. Como resultado, el optimizador puede asumir que p2->m = -p2->m;
no tiene efecto en p1->m
; primero puede leer el valor de p1->m
a un registro, compararlo con 0, si se compara con menos de 0, luego haga p2->m = -p2->m;
y finalmente devolver el valor de registro sin cambios!
La unión aquí es la única manera de hacer p1 == p2
en el nivel binario porque todos los miembros de la unión tienen la misma dirección.
Otro ejemplo:
struct t1 { int m; };
struct t2 { int m; };
int f(struct t1* p1, struct t2* p2)
{
if (p1->m < 0) p2->m = -p2->m;
return p1->m;
}
int g()
{
union {
struct t1 s1;
struct t2 s2;
} u;
u.s1.m = -1;
return f(&u.s1, &u.s2);
}
¿Qué debo devolver? +1
según el sentido común (cambiamos -1 a +1 en f
). Pero si nos fijamos en gcc''s genere ensamblado con optimización -O1
f:
cmp DWORD PTR [rdi], 0
js .L3
.L2:
mov eax, DWORD PTR [rdi]
ret
.L3:
neg DWORD PTR [rsi]
jmp .L2
g:
mov eax, 1
ret
Hasta ahora todo es como excepción. Pero cuando lo intentamos con -O2
f:
mov eax, DWORD PTR [rdi]
test eax, eax
js .L4
ret
.L4:
neg DWORD PTR [rsi]
ret
g:
mov eax, -1
ret
El valor de retorno ahora es un codificado por defecto -1
Esto se debe a que f
al principio almacena en caché el valor de p1->m
en el registro eax
( mov eax, DWORD PTR [rdi]
) y no lo vuelve a leer después de p2->m = -p2->m;
( neg DWORD PTR [rsi]
): devuelve eax
sin cambios.
la unión aquí utilizada solo para todos los datos no estáticos los miembros de un objeto de unión tienen la misma dirección. como resultado &u.s1 == &u.s2
.
es alguien que no entiende el código del ensamblador, puede mostrar en c / c ++ cómo el aliasing estricto afecta al código f:
int f(struct t1* p1, struct t2* p2)
{
int a = p1->m;
if (a < 0) p2->m = -p2->m;
return a;
}
el caché del compilador p1->m
valor en var a local (en realidad en el registro, por supuesto) y lo devuelve, a pesar de p2->m = -p2->m;
cambiar p1->m
. pero el compilador asume que la memoria p1
no se ve afectada, porque asume que p2
apunta a otra memoria que no se superpone con p1
así, con diferentes compiladores y diferentes niveles de optimización, el mismo código fuente puede devolver diferentes valores (-1 o +1). tal y comportamiento indefinido como es
Cuando leí ISO / IEC 9899: 1999 (ver: 6.5.2.3), vi un ejemplo como este (énfasis mío):
Lo siguiente no es un fragmento válido (porque el tipo de unión no es visible dentro de la función
f
):
struct t1 { int m; }; struct t2 { int m; }; int f(struct t1 * p1, struct t2 * p2) { if (p1->m < 0) p2->m = -p2->m; return p1->m; } int g() { union { struct t1 s1; struct t2 s2; } u; /* ... */ return f(&u.s1, &u.s2); }
No encontré errores y advertencias cuando hice la prueba.
Mi pregunta es: ¿Por qué este fragmento no es válido?
El ejemplo intenta ilustrar el párrafo 1 de antemano (énfasis mío):
6.5.2.3 ¶6
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 . Dos estructuras comparten una secuencia inicial común si los miembros correspondientes tienen tipos compatibles (y, para campos de bits, los mismos anchos) para una secuencia de uno o más miembros iniciales.
Como f
se declara antes de g
, y además el tipo de unión sin nombre es local a g
, no hay duda de que el tipo de unión no es visible en f
.
El ejemplo no muestra cómo se inicializa u
, pero si asumimos que el último escrito en el miembro es u.s2.m
, la función tiene un comportamiento indefinido porque inspecciona p1->m
sin que esté vigente la garantía de secuencia inicial común.
Lo mismo ocurre de otra manera, si es u.s1.m
que se escribió por última vez antes de la llamada a la función, el acceso a p2->m
es un comportamiento indefinido.
Tenga en cuenta que f
sí no es inválido. Es una definición de función perfectamente razonable. El comportamiento indefinido se deriva de pasar en él &u.s1
y &u.s2
como argumentos. Eso es lo que está causando un comportamiento indefinido.
1 - Estoy citando n1570 , el borrador estándar C11. Pero la especificación debe ser la misma, sujeta solo a mover un párrafo o dos arriba / abajo.
Uno de los propósitos principales de la regla de secuencia inicial común es permitir que las funciones operen en muchas estructuras similares de manera intercambiable. Requerir que los compiladores asuman que cualquier función que actúe sobre una estructura podría cambiar el miembro correspondiente en cualquier otra estructura que comparta una secuencia inicial común, sin embargo, habría afectado optimizaciones útiles.
Aunque la mayoría de los códigos que se basan en las garantías de la secuencia inicial común hacen uso de algunos patrones fácilmente reconocibles, por ejemplo
struct genericFoo {int size; short mode; };
struct fancyFoo {int size; short mode, biz, boz, baz; };
struct bigFoo {int size; short mode; char payload[5000]; };
union anyKindOfFoo {struct genericFoo genericFoo;
struct fancyFoo fancyFoo;
struct bigFoo bigFoo;};
...
if (readSharedMemberOfGenericFoo( myUnion->genericFoo ))
accessThingAsFancyFoo( myUnion->fancyFoo );
return readSharedMemberOfGenericFoo( myUnion->genericFoo );
revisando la unión entre llamadas a funciones que actúan sobre diferentes miembros de la unión, los autores de la Norma especificaron que la visibilidad del tipo de unión dentro de la función llamada debe ser el factor determinante para que las funciones reconozcan la posibilidad de que un acceso, por ejemplo, mode
de campo de un FancyFoo
podría afectar el mode
de campo de un genericFoo
. El requisito de tener una unión que contenga todos los tipos de estructuras cuya dirección podría pasarse a readSharedMemberOfGeneric
en la misma unidad de compilación que la función hace que la regla de secuencia inicial común sea menos útil de lo que sería, pero permitiría al menos algunos patrones como el arriba utilizable.
Sin embargo, los autores de gcc y clang pensaron que tratar las declaraciones de unión como una indicación de que los tipos involucrados podrían estar involucrados en construcciones como las anteriores sería un impedimento poco práctico para la optimización, y pensaron que dado que el Estándar no requiere que apoyen tales las construcciones a través de otros medios, simplemente no las apoyarán en absoluto. En consecuencia, el requisito real para el código que necesitaría explotar las garantías de la Secuencia Inicial Común de cualquier manera significativa no es garantizar que una declaración de tipo sindical sea visible, sino asegurar que se invocan clang y gcc con el -fno-strict-aliasing
bandera. Además, incluir una declaración de unión visible cuando sea práctico no afectaría, pero no es necesario ni suficiente para garantizar el comportamiento correcto de gcc y clang.