rubndsouza
Recientemente me encontré con el código de un colega que se veía así:
typedef struct A {
int x;
}A;
typedef struct B {
A a;
int d;
}B;
void fn(){
B *b;
((A*)b)->x = 10;
}
Su explicación fue que desde struct A
fue el primer miembro de struct B
asi que b->x
sería lo mismo que b->a.x
y proporciona una mejor legibilidad.
Esto tiene sentido, pero ¿se considera una buena práctica? ¿Y esto funcionará en todas las plataformas? Actualmente esto funciona bien en GCC.
paxdiablo
Sí, funcionará multiplataforma.(a)pero eso no necesariamente que sea una buena idea.
Según el estándar ISO C (todas las citas a continuación son de C11), 6.7.2.1 Structure and union specifiers /15
no se permite que haya relleno antes de el primer elemento de una estructura
Además, 6.2.7 Compatible type and composite type
Establece que:
Dos tipos tienen tipo compatible si sus tipos son iguales
y es indiscutible que el A
y A-within-B
los tipos son idénticos.
Esto significa que la memoria accede a la A
los campos serán los mismos en ambos A
y B
tipos, como lo harían los más sensatos b->a.x
que es probablemente lo que usted debería utilizar si tiene alguna duda sobre la mantenibilidad en el futuro.
Y, aunque normalmente tendría que preocuparse por el alias de tipo estricto, no creo que eso se aplique aquí. Eso es ilegal a punteros de alias, pero el estándar tiene excepciones específicas.
6.5 Expressions /7
establece algunas de esas excepciones, con la nota al pie:
La intención de esta lista es especificar aquellas circunstancias en las que un objeto puede o no tener un alias.
Las excepciones enumeradas son:
a type compatible with the effective type of the object
;- algunas otras excepciones que no nos interesan aquí; y
an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)
.
Eso, combinado con las reglas de relleno de estructura mencionadas anteriormente, incluida la frase:
Un puntero a un objeto de estructura, convenientemente convertido, apunta a su miembro inicial
parece indicar que este ejemplo está específicamente permitido. El punto central que tenemos que recordar aquí es que el tipo de expresión ((A*)b)
es A*
no B*
. Eso hace que las variables sean compatibles a los efectos de la creación de alias sin restricciones.
Esa es mi lectura de las partes relevantes del estándar, me he equivocado antes (b)pero lo dudo en este caso.
Entonces, si tienes un auténtico necesito esto, funcionará bien, pero estaría documentando cualquier restricción en el código muy cerca de las estructuras para no ser mordido en el futuro.
(a) En el sentido general. Por supuesto, el fragmento de código:
B *b;
((A*)b)->x = 10;
será un comportamiento indefinido porque b
no se inicializa a algo sensato. Pero voy a suponer que esto es solo un código de ejemplo destinado a ilustrar su pregunta. Si a alguien le preocupa, piénsalo como:
B b, *pb = &b;
((A*)pb)->x = 10;
(b) Como mi esposa le dirá, con frecuencia y con pocas indicaciones 🙂
relajarse
Me arriesgaré y me opondré a @paxdiablo en este caso: creo que es una buena idea, y es muy común en código grande con calidad de producción.
Es básicamente la forma más obvia y agradable de implementar estructuras de datos orientadas a objetos basadas en herencia en C. Comenzar la declaración de struct B
con una instancia de struct A
significa “B es una subclase de A”. El hecho de que se garantice que el primer miembro de la estructura tenga 0 bytes desde el comienzo de la estructura es lo que hace que funcione de manera segura, y en mi opinión, está en el límite de la belleza.
Es ampliamente utilizado y desplegado en código basado en el GObjeto biblioteca, como el kit de herramientas de interfaz de usuario GTK+ y el entorno de escritorio GNOME.
Por supuesto, requiere que “sepa lo que está haciendo”, pero generalmente ese es siempre el caso cuando se implementan relaciones de tipo complicadas en C. 🙂
En el caso de GObject y GTK+, hay mucha infraestructura de soporte y documentación para ayudar con esto: es bastante difícil olvidarse de eso. Puede significar que crear una nueva clase no es algo que se haga tan rápido como en C++, pero tal vez eso sea de esperar ya que no hay soporte nativo en C para las clases.
-
Más que hermoso en el límite, está más en el límite del comportamiento impredecible. “El hecho de que se garantice que el primer miembro de la estructura tenga 0 bytes desde el inicio de la estructura…” podría cambiar según la implementación del compilador. ¿Es un estándar de C++? Incluso si fuera parte del estándar, podría causar dolores de cabeza de mantenimiento para personas desconocidas en el código base, ¡que primero tendrían que descubrir los tipos!
– Atmaram Shetye
27 de octubre de 2017 a las 8:56
Esa es una idea horrible. Tan pronto como alguien aparece e inserta otro campo al principio de la estructura B, tu programa explota. ¿Y qué tiene de malo b.a.x
?
Dureuill
En general, se debe evitar cualquier cosa que eluda la verificación de tipos. Este truco se basa en el orden de las declaraciones y el compilador no puede aplicar ni la conversión ni este orden.
Debería funcionar multiplataforma, pero no creo que sea una buena práctica.
Si realmente tiene estructuras profundamente anidadas (sin embargo, es posible que tenga que preguntarse por qué), entonces debe usar una variable local temporal para acceder a los campos:
A deep_a = e->d.c.b.a;
deep_a.x = 10;
deep_a.y = deep_a.x + 72;
e->d.c.b.a = deep_a;
O, si no quieres copiar a
a lo largo de:
A* deep_a = &(e->d.c.b.a);
deep_a->x = 10;
deep_a->y = deep_a->x + 72;
Esto muestra de dónde a
viene y no requiere yeso.
Java y C # también exponen regularmente construcciones como “cba”, no veo cuál es el problema. Si lo que quiere simular es un comportamiento orientado a objetos, entonces debería considerar usar un lenguaje orientado a objetos (como C++), ya que “extender estructuras” en la forma que propone no proporciona encapsulación ni polimorfismo en tiempo de ejecución (aunque se puede argumentar que ((A*)b) es similar a un “reparto dinámico”).
Lamento no estar de acuerdo con todas las otras respuestas aquí, pero este sistema no cumple con el estándar C. No es aceptable tener dos punteros con diferentes tipos que apunten a la misma ubicación al mismo tiempo, esto se llama aliasing y es no permitido por las estrictas reglas de alias en C99 y muchos otros estándares. Una forma menos fea de hacer esto sería usar funciones getter en línea que luego no tienen que verse ordenadas de esa manera. ¿O tal vez este es el trabajo de un sindicato? Específicamente permitido sostener uno de varios tipos, sin embargo, también hay una gran cantidad de otros inconvenientes.
En resumen, este tipo de conversión sucia para crear polimorfismo no está permitido por la mayoría de los estándares de C, solo porque parece funcionar en su compilador no significa que sea aceptable. Consulte aquí para obtener una explicación de por qué no está permitido y por qué los compiladores con altos niveles de optimización pueden descifrar el código que no sigue estas reglas. http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization
-
En realidad, en el uso descrito, no es un alias y cumple explícitamente con C99 6.7.2.1p13: “Un puntero a un objeto de estructura, convenientemente convertido, apunta a su miembro inicial, y viceversa.”
– Greg A. Woods
7 de marzo de 2016 a las 2:30
Comunidad
Sí, funcionará. Y es uno de los principios básicos de Orientado a objetos usando C. Consulte esta respuesta ‘Orientación a objetos en C’ para obtener más ejemplos sobre la extensión (es decir, la herencia).
-
En realidad, en el uso descrito, no es un alias y cumple explícitamente con C99 6.7.2.1p13: “Un puntero a un objeto de estructura, convenientemente convertido, apunta a su miembro inicial, y viceversa.”
– Greg A. Woods
7 de marzo de 2016 a las 2:30
patricio collins
Esto es perfectamente legal y, en mi opinión, bastante elegante. Para ver un ejemplo de esto en el código de producción, consulte el Documentos de GObject:
Gracias a estas simples condiciones, es posible detectar el tipo de cada instancia de objeto haciendo:
B *b; b->parent.parent.g_class->g_type
o, más rápidamente:
B *b; ((GTypeInstance*)b)->g_class->g_type
Personalmente, creo que los sindicatos son feos y tienden a conducir a enormes switch
declaraciones, que es una gran parte de lo que ha trabajado para evitar al escribir código OO. Yo mismo escribo una cantidad significativa de código en este estilo — normalmente, el primer miembro de la struct
contiene punteros de función que pueden funcionar como una tabla virtual para el tipo en cuestión.