Casting des pointeurs de structure entre des structures contenant des pointeurs vers différents types?

J’ai une structure, définie comme suit:

struct vector { (TYPE) *items; size_t nitems; }; 

où type peut être littéralement n’importe quel type et que je possède une structure de type similaire au type:

 struct _vector_generic { void *items; size_t nitems; }; 

La seconde structure est utilisée pour passer des structures du premier type de tout type à une fonction de redimensionnement, par exemple comme ceci:

 struct vector v; vector_resize((_vector_generic*)&v, sizeof(*(v->items)), v->nitems + 1); 

vector_resize tente de vector_resize de la mémoire pour le nombre donné d’éléments dans le vecteur.

 int vector_resize (struct _vector_generic *v, size_t item_size, size_t length) { void *new = realloc(v->items, item_size * length); if (!new) return -1; v->items = new; v->nitems = length; return 0; } 

Cependant, la norme C stipule qu’il n’est pas nécessaire que les pointeurs vers des types différents aient la même taille.

6.2.5.27:

Un pointeur sur void doit avoir les mêmes exigences de représentation et d’alignement qu’un pointeur sur un type de caractère.39) De même, les pointeurs sur des versions qualifiées ou non qualifiées de types compatibles doivent avoir les mêmes exigences de représentation et d’alignement. Tous les pointeurs sur les types de structure doivent avoir les mêmes exigences de représentation et d’alignement. Tous les pointeurs sur les types d’union doivent avoir les mêmes exigences de représentation et d’alignement. Les pointeurs vers d’autres types ne doivent pas nécessairement avoir les mêmes exigences de représentation ou d’alignement.

Maintenant, ma question est la suivante: devrais-je craindre que ce code ne tombe sur certaines architectures?

Puis-je résoudre ce problème en réorganisant mes structures de telle sorte que le type de pointeur se trouve à la fin? par exemple:

 struct vector { size_t nitems; (TYPE) *items; }; 

Et si non, que puis-je faire?

Pour une référence à ce que j’essaie de réaliser, voir:
https://github.com/andy-graprof/grapes/blob/master/grapes/vector.h

Pour un exemple d’utilisation, voir:
https://github.com/andy-graprof/grapes/blob/master/tests/grapes.tests/vector.exp

Votre code n’est pas défini.

L’access à un object à l’aide d’une valeur de type incompatible entraîne un comportement indéfini.

La norme définit ceci dans:

6,5 p7:

Un object doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui possède l’un des types suivants:

– un type compatible avec le type effectif de l’object,

– une version qualifiée d’un type compatible avec le type effectif de l’object,

– un type qui est le type signé ou non signé correspondant au type effectif de l’object,

– un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l’object,

– un type d’agrégat ou d’union qui inclut l’un des types mentionnés ci-dessus parmi ses membres (y compris, de manière récursive, un membre d’une union sous-agrégée ou confinée), ou

– un type de caractère.

struct vector et struct _vector_generic ont des types incompatibles et ne rentrent dans aucune des catégories ci-dessus. Leur représentation interne est sans importance dans ce cas.

Par exemple:

 struct vector v; _vector_generic* g = &v; g->size = 123 ; //undefined! 

Il en va de même pour votre exemple où vous passez l’adresse du vecteur struct à la fonction et l’interprétez comme un pointeur _vector_generic.

Les tailles et le remplissage des structures peuvent également être différents, ce qui entraîne le positionnement des éléments à différents décalages.

Ce que vous pouvez faire, c’est utiliser votre structure générique et transtyper si, en fonction du type, le pointeur vide contient le code principal.

 struct gen { void *items; size_t nitems; size_t nsize ; }; struct gen* g = malloc( sizeof(*g) ) ; g->nitems = 10 ; g->nsize = sizeof( float ) ; g->items = malloc( g->nsize * g->nitems ) ; float* f = g->items ; f[g->nitems-1] = 1.2345f ; ... 

En utilisant la même définition de structure, vous pouvez affecter un type différent:

 struct gen* g = malloc( sizeof(*g) ) ; g->nitems = 10 ; g->nsize = sizeof( int ) ; g->items = malloc( g->nsize * g->nitems ) ; int* i = g->items ; ... 

Puisque vous enregistrez la taille du type et le nombre d’éléments, il est évident que votre fonction de redimensionnement ressemblera (essayez-le).

Veillez à vous rappeler quel type est utilisé dans quelle variable car le compilateur ne vous préviendra pas car vous utilisez void *.

Le code de votre question appelle un comportement indéfini (UB), car vous dé-référencez un pointeur potentiellement non valide. Le casting:

 (_vector_generic*)&v 

… est couvert par 6.3.2.3 paragraphe 7:

Un pointeur sur un type d’object peut être converti en un pointeur sur un type d’object différent. Si le pointeur résultant n’est pas correctement aligné pour le type référencé, le comportement est indéfini. Sinon, lorsqu’il sera reconverti, le résultat sera comparable au pointeur original.

Si nous supposons que les conditions d’alignement sont remplies, la conversion n’appelle pas UB. Cependant, il n’est pas nécessaire que le pointeur converti soit “comparable” avec (c’est-à-dire qu’il pointe vers le même object que) le pointeur d’origine, ni même qu’il pointe vers un object quelconque, c’est-à-dire avec la valeur du pointeur. n’est pas spécifié – par conséquent, pour déréférencer ce pointeur (sans d’abord vérifier qu’il est égal à l’original), il invoque un comportement non défini.

(Beaucoup de gens qui connaissent bien C trouvent cela étrange. Je pense que c’est parce qu’ils savent qu’une conversion de pointeur ne comstack généralement aucune opération – la valeur du pointeur rest simplement telle qu’elle est – et ils voient donc la conversion de pointeur comme une conversion de type purement. Cependant, la norme ne l’impose pas).

Même si le pointeur après conversion était bien égal au pointeur original, le paragraphe 6.5 (ce que l’on appelle la “règle de pseudonyme ssortingcte”) ne vous permettrait pas de le déréférencer. Pour l’essentiel, vous ne pouvez pas accéder au même object via deux pointeurs de type différent, à quelques exceptions près.

Exemple:

 struct a { int n; }; struct b { int member; }; struct a a_object; struct b * bp = (struct b *) &a_object; // bp takes an unspecified value // Following would invoke UB, because bp may be an invalid pointer: // int m = b->member; // But what if we can ascertain that bp points at the original object?: if (bp == &a_object) { // The comparison in the line above actually violates constraints // in 6.5.9p2, but it is accepted by many comstackrs. int m = b->member; // UB if executed, due to 6.5p7. } 

Permet, aux fins de discussion, d’ignorer que le standard C dit formellement qu’il s’agit d’un comportement indéfini. Parce qu’un comportement indéfini signifie simplement que quelque chose dépasse le cadre de la norme de langage: tout peut arriver et la norme C ne donne aucune garantie. Il peut cependant y avoir des garanties “externes” sur le système que vous utilisez, fournies par ceux qui en ont fait le système.

Et dans le monde réel où il y a du matériel, il existe effectivement de telles garanties. Il y a juste deux choses qui peuvent aller mal ici dans la pratique:

  • TYPE* ayant une représentation ou une taille différente de void* .
  • Remplissage de structure différent dans chaque type de structure en raison des exigences d’alignement.

Ces deux éléments semblent improbables et peuvent être évités avec une affirmation statique:

 static void ct_assert (void) // dummy function never linked or called by anyone { struct vector v1; struct _vector_generic v2; static_assert(sizeof(v1.items) == sizeof(v2.items), "Err: unexpected pointer format."); static_assert(sizeof(v1) == sizeof(v2), "Err: unexpected padding."); } 

Maintenant, la seule chose qui pourrait mal tourner est si un “pointeur sur x” a la même taille mais une représentation différente par rapport à “pointeur sur y” sur votre système spécifique. Je n’ai jamais entendu parler d’un tel système, nulle part dans le monde réel. Mais bien sûr, il n’ya aucune garantie: des systèmes aussi obscurs et non orthodoxes peuvent exister. Dans ce cas, il vous appartient de décider si vous souhaitez les prendre en charge ou s’il suffit de disposer de la portabilité à 99,99% de tous les ordinateurs existants dans le monde.

En pratique, un système ne dispose de plusieurs formats de pointeur que lorsque vous adressez de la mémoire au-delà de la largeur d’adresse standard de la CPU, qui est généralement gérée par des extensions non standard, telles que des pointeurs far . Dans tous ces cas, les pointeurs auront des tailles différentes et vous détecterez de tels cas avec l’assertion statique ci-dessus.