Puede haber muchas ventajas en dicho código, pero desafortunadamente el Estándar C no fue escrito para facilitarlo. Históricamente, los compiladores han ofrecido garantías de comportamiento efectivas más allá de lo que requería el Estándar, lo que hizo posible escribir dicho código de manera mucho más limpia de lo que es posible en el Estándar C, pero los compiladores han comenzado recientemente a revocar tales garantías en nombre de la optimización.
En particular, muchos compiladores de C han garantizado históricamente (por diseño, si no documentación) que si dos tipos de estructura contienen la misma secuencia inicial, se puede usar un puntero a cualquiera de los tipos para acceder a los miembros de esa secuencia común, incluso si los tipos no están relacionados, y además que para propósitos de establecer una secuencia inicial común, todos los apuntadores a estructuras son equivalentes. El código que hace uso de este comportamiento puede ser mucho más limpio y más seguro de tipo que el código que no lo hace, pero desafortunadamente, aunque el Estándar requiere que las estructuras que comparten una secuencia inicial común deben establecerse de la misma manera, prohíbe que el código realmente use un puntero de un tipo para acceder a la secuencia inicial de otro.
En consecuencia, si desea escribir código orientado a objetos en C, tendrá que decidir (y debe tomar esta decisión desde el principio) saltar entre muchos aros para cumplir con las reglas de tipo puntero de C y estar preparado para tener los compiladores modernos generan código sin sentido si uno se desliza hacia arriba, incluso si los compiladores más antiguos hubieran generado un código que funciona según lo previsto, o de lo contrario documentan el requisito de que el código solo se pueda usar con compiladores que estén configurados para admitir el comportamiento del puntero de estilo antiguo (por ejemplo, usando un "-fno-estricto-alias") Algunas personas consideran que "-fno-estricto-aliasing" es malo, pero sugeriría que es más útil pensar en "-fno-estricto-aliasing" C como un lenguaje que ofrece mayor poder semántico para algunos propósitos que el "estándar" C,pero a expensas de optimizaciones que pueden ser importantes para otros propósitos.
A modo de ejemplo, en los compiladores tradicionales, los compiladores históricos interpretarían el siguiente código:
struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };
void hey(struct pair *p, struct trio *t)
{
p->i1++;
t->i1^=1;
p->i1--;
t->i1^=1;
}
siguiendo los siguientes pasos en orden: incremente el primer miembro de *p
, complemente el bit más bajo del primer miembro de *t
, luego disminuya el primer miembro de *p
y complemente el bit más bajo del primer miembro de *t
. Los compiladores modernos reorganizarán la secuencia de operaciones de una manera que codifique, que será más eficiente p
e t
identificará diferentes objetos, pero cambiará el comportamiento si no lo hacen.
Este ejemplo es, por supuesto, deliberadamente ideado, y en la práctica el código que usa un puntero de un tipo para acceder a miembros que son parte de la secuencia inicial común de otro tipo generalmente funcionará, pero desafortunadamente ya que no hay forma de saber cuándo podría fallar dicho código no es posible usarlo de manera segura, excepto deshabilitando el análisis de alias basado en tipo.
Un ejemplo algo menos artificial ocurriría si uno quisiera escribir una función para hacer algo como cambiar dos punteros a tipos arbitrarios. En la gran mayoría de los compiladores "C de la década de 1990", eso podría lograrse mediante:
void swap_pointers(void **p1, void **p2)
{
void *temp = *p1;
*p1 = *p2;
*p2 = temp;
}
En el Estándar C, sin embargo, uno debería usar:
#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
void **temp = malloc(sizeof (void*));
memcpy(temp, p1, sizeof (void*));
memcpy(p1, p2, sizeof (void*));
memcpy(p2, temp, sizeof (void*));
free(temp);
}
Si *p2
se mantiene en el almacenamiento asignado y el puntero temporal no se mantiene en el almacenamiento asignado, el tipo efectivo de *p2
se convertirá en el tipo del puntero temporal y el código que intenta usar *p2
como cualquier tipo que no coincida con el puntero temporal type invocará Comportamiento indefinido. Es seguro que es extremadamente improbable que un compilador se dé cuenta de tal cosa, pero dado que la filosofía moderna del compilador requiere que los programadores eviten el Comportamiento Indefinido a toda costa, no puedo pensar en ningún otro medio seguro de escribir el código anterior sin usar el almacenamiento asignado .