Il peut y avoir de nombreux avantages à ce code, mais malheureusement, la norme C n'a pas été écrite pour le faciliter. Les compilateurs ont historiquement offert des garanties de comportement efficaces au-delà de ce que la norme exigeait qui permettait d'écrire un tel code beaucoup plus proprement que ce qui est possible dans la norme C, mais les compilateurs ont récemment commencé à révoquer ces garanties au nom de l'optimisation.
Plus particulièrement, de nombreux compilateurs C ont historiquement garanti (par conception sinon documentation) que si deux types de structure contiennent la même séquence initiale, un pointeur vers l'un ou l'autre type peut être utilisé pour accéder aux membres de cette séquence commune, même si les types ne sont pas liés, et en outre qu'aux fins de l'établissement d'une séquence initiale commune, tous les pointeurs vers les structures sont équivalents. Le code qui utilise un tel comportement peut être beaucoup plus propre et plus sûr que le code qui ne le fait pas, mais malheureusement, même si la norme exige que les structures partageant une séquence initiale commune doivent être disposées de la même manière, il interdit au code d'utiliser réellement un pointeur d'un type pour accéder à la séquence initiale d'un autre.
Par conséquent, si vous voulez écrire du code orienté objet en C, vous devrez décider (et devriez prendre cette décision tôt) de sauter à travers un grand nombre de cerceaux pour respecter les règles de type pointeur de C et être prêt à avoir les compilateurs modernes génèrent du code insensé si l'un d'eux glisse, même si des compilateurs plus anciens auraient généré du code qui fonctionne comme prévu, ou bien documentent une exigence selon laquelle le code ne sera utilisable qu'avec des compilateurs configurés pour prendre en charge le comportement de pointeur à l'ancienne (par exemple en utilisant un "-fno-strict-aliasing") Certaines personnes considèrent "-fno-strict-aliasing" comme mauvais, mais je dirais qu'il est plus utile de penser à "-fno-strict-aliasing" C comme étant un langage qui offre une puissance sémantique supérieure à certaines fins que le C "standard",mais au détriment des optimisations qui pourraient être importantes à d'autres fins.
À titre d'exemple, sur les compilateurs traditionnels, les compilateurs historiques interpréteraient le code suivant:
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;
}
comme effectuer les étapes suivantes dans l'ordre: incrémenter le premier membre de *p
, compléter le bit le plus bas du premier membre de *t
, puis décrémenter le premier membre de *p
, et compléter le bit le plus bas du premier membre de *t
. Les compilateurs modernes réorganiseront la séquence des opérations de manière à coder qui sera plus efficace p
et t
identifiera les différents objets, mais changeront le comportement s'ils ne le font pas.
Cet exemple est bien sûr délibérément conçu, et dans la pratique, le code qui utilise un pointeur d'un type pour accéder aux membres qui font partie de la séquence initiale commune d'un autre type fonctionnera généralement , mais malheureusement puisqu'il n'y a aucun moyen de savoir quand un tel code pourrait échouer. il n'est pas possible de l'utiliser en toute sécurité, sauf en désactivant l'analyse d'alias basée sur le type.
Un exemple un peu moins artificiel se produirait si l'on voulait écrire une fonction pour faire quelque chose comme échanger deux pointeurs vers des types arbitraires. Dans la grande majorité des compilateurs «C des années 90», cela pourrait être accompli via:
void swap_pointers(void **p1, void **p2)
{
void *temp = *p1;
*p1 = *p2;
*p2 = temp;
}
Dans la norme C, cependant, il faudrait utiliser:
#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
est conservé dans le stockage alloué et que le pointeur temporaire n'est pas conservé dans le stockage alloué, le type effectif de *p2
deviendra le type du pointeur temporaire et le code qui tentera de l'utiliser *p2
comme n'importe quel type ne correspondant pas au pointeur temporaire type invoquera le comportement indéfini. Il est certain qu'il est extrêmement improbable qu'un compilateur remarque une telle chose, mais comme la philosophie moderne du compilateur exige que les programmeurs évitent à tout prix les comportements indéfinis, je ne peux penser à aucun autre moyen sûr d'écrire le code ci-dessus sans utiliser le stockage alloué. .