resueltos - Estructuras anidadas y aliasing estricto en c
struct c++ para que sirve (4)
La regla de alias estricta es aproximadamente dos punteros de diferentes tipos que hacen referencia a la misma ubicación en la memoria (ISO/IEC9899/TC2) . Aunque su ejemplo reinterpreta la dirección del object_t object
como una dirección de person_t
, no hace referencia a la ubicación de la memoria dentro de object_t
través del puntero reinterpretado, porque la age
se encuentra más allá del límite de object_t
. Como las ubicaciones de memoria a las que se hace referencia a través de los punteros no son las mismas, diría que no viola la regla estricta de aliasing. FWIW, gcc -fstrict-aliasing -Wstrict-aliasing=2 -O3 -std=c99
parece estar de acuerdo con esa evaluación, y no produce una advertencia.
Sin embargo, esto no es suficiente para decidir que se trata de un código legal: su ejemplo supone que la dirección de una estructura anidada es la misma que la de su estructura externa. Por cierto, esta es una suposición segura de hacer de acuerdo con el estándar C99:
6.7.2.1-13. Un puntero a un objeto de estructura, adecuadamente convertido, apunta a su miembro inicial
Las dos consideraciones anteriores me hacen pensar que su código es legal.
Por favor considere el siguiente código:
typedef struct {
int type;
} object_t;
typedef struct {
object_t object;
int age;
} person_t;
int age(object_t *object) {
if (object->type == PERSON) {
return ((person_t *)object)->age;
} else {
return 0;
}
}
¿Es este código legal o está violando la regla de alias estricta de C99? Por favor explique por qué es legal / ilegal.
La regla de alias estricta limita por qué tipos accede a un objeto (una región de la memoria). Hay algunos lugares en el código donde la regla podría surgir: dentro de age()
y al llamar a age()
.
Dentro de la age
, tienes un object
a tener en cuenta. ((person_t *)object)
es una expresión lvalue porque tiene un tipo de objeto y designa un objeto (una región de memoria). Sin embargo, la rama solo se alcanza si object->type == PERSON
, por lo que (presumiblemente) el tipo efectivo del objeto es person_t*
, por lo tanto, la person_t*
no viola el aliasing estricto. En particular, el alias estricto permite:
- un tipo compatible con el tipo efectivo del objeto,
Al llamar a age()
, probablemente pasará un object_t*
o un tipo que desciende de object_t
: una estructura que tiene un object_t
como el primer miembro. Esto se permite como:
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros
Además, el objetivo de un alias estricto es permitir la optimización de los valores de carga en los registros. Si un objeto se muta a través de un puntero, se asume que cualquier cosa apuntada por punteros de un tipo incompatible no se modificará y, por lo tanto, no es necesario volver a cargarla. El código no modifica nada, por lo que no debería verse afectado por la optimización.
Una forma aceptable que está explícitamente aprobada por la norma es hacer una unión de estructuras con un segmento inicial idéntico, de esta manera:
struct tag { int value; };
struct obj1 { int tag; Foo x; Bar y; };
struct obj2 { int tag; Zoo z; Car w; };
typedef union object_
{
struct tag;
struct obj1;
struct obj2;
} object_t;
Ahora puede pasar un object_t * p
y examinar p->tag.value
con impunidad, y luego acceder al miembro de la unión deseado.
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
Como complemento de la respuesta aceptada, aquí está la cita completa de la norma, con la parte importante resaltada que la otra respuesta se omite, y una más:
6.7.2.1-13: Dentro de un objeto de estructura, los miembros que no son de campo de bits y las unidades en que residen los campos de bits tienen direcciones que aumentan en el orden en que se declaran. Un puntero a un objeto de estructura, adecuadamente convertido, apunta a su miembro inicial (o si ese miembro es un campo de bits, luego a la unidad en la que reside), y viceversa . Puede haber un relleno sin nombre dentro de un objeto de estructura, pero no al principio.
6.3.2.3-7: Un puntero a un objeto o tipo incompleto se puede convertir en un puntero a un objeto diferente o tipo incompleto. Si el puntero resultante no está alineado correctamente para el tipo apuntado a, el comportamiento no está definido. De lo contrario, cuando se vuelva a convertir, el resultado se comparará igual al puntero original. [...]
Considero que su ejemplo es un lugar perfecto para un indicador de vacío:
int age(void *object) {
¿Por qué? Debido a que su intención obvia es otorgar diferentes tipos de "objetos" a dicha función, y obtiene la información de acuerdo con el tipo codificado. En su versión, necesita un lanzamiento cada vez que llame a la función: age((object_t*)person);
. El compilador no se quejará cuando le des el puntero equivocado, por lo que no hay ningún tipo de seguridad involucrado, de todos modos. Luego, también puede usar un puntero de vacío y evitar el lanzamiento cuando llame a la función.
Alternativamente, puedes llamar a la función con age(&person->object)
, por supuesto. Cada vez que lo llames.