Extension d’une structure en C

Je suis récemment tombé sur le code d’un collègue qui ressemblait à ceci:

typedef struct A { int x; }A; typedef struct B { A a; int d; }B; void fn(){ B *b; ((A*)b)->x = 10; } 

Son explication était que puisque struct A était le premier membre de struct B , b->x serait donc le même que b->ax et fournirait une meilleure lisibilité.
Cela a du sens, mais est-ce considéré comme une bonne pratique? Et cela fonctionnera-t-il sur plusieurs plates-formes? Actuellement, cela fonctionne bien sur GCC.

Oui, cela fonctionnera sur plusieurs plates-formes, mais cela ne signifie pas nécessairement que ce soit une bonne idée.

Selon la norme ISO C (toutes les citations ci-dessous sont de C11), 6.7.2.1 Structure and union specifiers /15 , il n’est pas permis de remplir avant le premier élément d’une structure

De plus, 6.2.7 Compatible type and composite type indique que:

Deux types ont un type compatible si leurs types sont identiques

et il n’est pas contesté que les types A et A-within-B sont identiques.

Cela signifie que les access mémoire aux champs A seront les mêmes dans les deux types A et B , de même que le plus sensible b->ax qui est probablement ce que vous devriez utiliser si vous avez des inquiétudes sur la maintenabilité à l’avenir.

Et bien que vous deviez normalement vous soucier de l’alias de type ssortingct, je ne crois pas que cela s’applique ici. Il est illégal d’utiliser des pointeurs d’alias, mais la norme a des exceptions spécifiques.

6.5 Expressions /7 indique certaines de ces exceptions, avec la note de bas de page:

Le but de cette liste est de spécifier les circonstances dans lesquelles un object peut ou non être aliasé.

Les exceptions répertoriées sont les suivantes:

  • a type compatible with the effective type of the object ;
  • d’autres exceptions qui ne doivent pas nous concerner ici; et
  • 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) .

Cela, combiné avec les règles de remplissage de la structure mentionnées ci-dessus, y compris la phrase:

Un pointeur sur un object de structure, convenablement converti, pointe vers son membre initial

semble indiquer que cet exemple est spécifiquement autorisé. Le point essentiel à retenir ici est que le type de l’expression ((A*)b) est A* , pas B* . Cela rend les variables compatibles pour les alias sans ressortingction.

C’est ce que je lis des parties pertinentes de la norme, je me suis trompé avant (a) , mais j’en doute dans ce cas.

Donc, si vous en avez vraiment besoin, cela fonctionnera bien mais je documenterais toutes les contraintes du code très proches des structures pour ne pas être mordues à l’avenir.


(a) Comme ma femme vous le dira, fréquemment et sans trop d’incitation 🙂

Je vais aller de l’avant et opposer @paxdiablo à celui-ci: je pense que c’est une bonne idée, et c’est très courant dans les grands codes de qualité de production.

C’est fondamentalement la manière la plus évidente et la plus simple d’implémenter des structures de données orientées object basées sur l’inheritance en C. Commencer la déclaration de struct B avec une instance de struct A signifie que “B est une sous-classe de A”. Le fait que le premier membre de la structure soit garanti à 0 octet au début de la structure est ce qui le fait fonctionner en toute sécurité, et à mon avis, il est beau.

Il est largement utilisé et déployé dans du code basé sur la bibliothèque GObject , comme le toolkit d’interface utilisateur GTK + et l’environnement de bureau GNOME.

Bien sûr, cela vous oblige à “savoir ce que vous faites”, mais c’est généralement toujours le cas lorsque vous implémentez des relations de type complexes en C. 🙂

Dans le cas de GObject et de GTK +, l’infrastructure de support et la documentation sont nombreuses: il est assez difficile de les oublier. Cela pourrait signifier que créer une nouvelle classe n’est pas quelque chose que vous faites aussi rapidement qu’en C ++, mais c’est peut-être à prévoir, car il n’y a pas de support natif dans C pour les classes.

Tout ce qui contourne la vérification de type doit généralement être évité. Ce hack repose sur l’ordre des déclarations et ni le cast, ni cet ordre ne peuvent être imposés par le compilateur.

Cela devrait fonctionner sur plusieurs plateformes, mais je ne pense pas que ce soit une bonne pratique.

Si vous avez vraiment des structures profondément nestedes (vous pourriez vous demander pourquoi, cependant), alors vous devriez utiliser une variable locale temporaire pour accéder aux champs:

 A deep_a = e->dcba; deep_a.x = 10; deep_a.y = deep_a.x + 72; e->dcba = deep_a; 

Ou, si vous ne voulez pas copier a long:

 A* deep_a = &(e->dcba); deep_a->x = 10; deep_a->y = deep_a->x + 72; 

Cela montre d’où vient et ne nécessite pas un casting.

Java et C # exposent aussi régulièrement des constructions comme “cba”, je ne vois pas quel est le problème. Si ce que vous voulez simuler est un comportement orienté object, alors vous devriez envisager d’utiliser un langage orienté object (comme C ++), car “étendre les structs” de la manière proposée ne fournit pas d’encapsulation ni de polymorphism d’exécution que ((A *) b) s’apparente à une “dissortingbution dynamic”).

C’est une idée horrible. Dès que quelqu’un arrive et insère un autre champ à l’avant de la structure B, votre programme explose. Et qu’est-ce qui ne va pas avec bax ?

Je suis désolé d’être en désaccord avec toutes les autres réponses ici, mais ce système n’est pas conforme à la norme C. Il n’est pas acceptable d’avoir deux pointeurs avec des types différents qui pointent vers le même emplacement en même temps. interdit par les règles ssortingctes d’aliasing en C99 et de nombreuses autres normes. Ce qui serait moins grave, c’est d’utiliser des fonctions de lecture en ligne qui n’auraient pas à se présenter de la sorte. Ou peut-être que c’est le travail d’un syndicat? Spécifiquement autorisé à contenir plusieurs types, il y a cependant une myriade d’autres inconvénients.

En bref, ce genre de casting sale pour créer un polymorphism n’est pas autorisé par la plupart des standards C, simplement parce qu’il semble fonctionner sur votre compilateur ne signifie pas qu’il est acceptable. Voir ici pour une explication de pourquoi il n’est pas autorisé, et pourquoi les compilateurs à des niveaux d’optimisation élevés peuvent casser le code qui ne suit pas ces règles http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization

Oui, ça va marcher. Et c’est l’un des principes fondamentaux d’ Object Oriented utilisant C. Voir cette réponse ” Orientation object dans C ” pour plus d’exemples sur l’extension (c.-à-d. L’inheritance).

C’est parfaitement légal et, à mon avis, très élégant. Pour un exemple de cela dans le code de production, voir les documents GObject :

Grâce à ces conditions simples, il est possible de détecter le type de chaque instance d’object en procédant comme suit:

 B *b; b->parent.parent.g_class->g_type 

ou plus rapidement:

 B *b; ((GTypeInstance*)b)->g_class->g_type 

Personnellement, je pense que les syndicats sont laids et tendent à conduire à des déclarations de switch énormes, ce qui est une grande partie de ce que vous avez travaillé à éviter en écrivant du code OO. J’écris moi-même une quantité importante de code dans ce style. En général, le premier membre de la struct contient des pointeurs de fonction qui peuvent être utilisés comme une vtable pour le type en question.

Je peux voir comment cela fonctionne mais je n’appellerais pas cette bonne pratique. Cela dépend de la manière dont les octets de chaque structure de données sont placés en mémoire. Chaque fois que vous transformez une structure de données compliquée en une autre (ex: structs), ce n’est pas une très bonne idée, surtout lorsque les deux structures ne sont pas de la même taille.

Je pense que le PO et de nombreux commentateurs ont compris que le code étendait une structure.

Ce n’est pas.

C’est et exemple de composition. Très utile. (Se débarrasser des typedefs, voici un exemple plus descriptif):

 struct person { char name[MAX_STRING + 1]; char address[MAX_STRING + 1]; } struct item { int x; }; struct accessory { int y; }; /* fixed size memory buffer. The Linux kernel is full of embedded structs like this */ struct order { struct person customer; struct item items[MAX_ITEMS]; struct accessory accessories[MAX_ACCESSORIES]; }; void fn(struct order *the_order){ memcpy(the_order->customer.name, DEFAULT_NAME, sizeof(DEFAULT_NAME)); } 

Vous avez un tampon de taille fixe qui est bien compartimenté. Il bat certainement une structure géante à un seul niveau.

 struct double_order { struct order order; struct item extra_items[MAX_ITEMS]; struct accessory extra_accessories[MAX_ACCESSORIES]; }; 

Donc, maintenant, vous avez une deuxième structure qui peut être traitée (a l’inheritance) exactement comme la première avec une dissortingbution explicite.

 struct double_order d; fn((order *)&d); 

Cela préserve la compatibilité avec le code écrit pour fonctionner avec la plus petite structure. Tant le kernel Linux ( http://lxr.free-electrons.com/source/include/linux/spi/spi.h (voir struct spi_device)) que la bibliothèque de sockets bsd ( http://beej.us/guide/ bgnet / output / html / multipage / sockaddr_inman.html ) utilisez cette approche. Dans les cas du kernel et des sockets, vous avez une structure qui est exécutée à la fois par des sections de code génériques et différenciées. Pas si différent que le cas d’utilisation de l’inheritance.

Je ne suggère PAS d’écrire des structures comme celle-là uniquement pour la lisibilité.

Je pense que Postgres le fait également dans certains de ses codes. Non pas que cela en fasse une bonne idée, mais cela en dit long sur son acceptation générale.

Vous pouvez peut-être envisager d’utiliser des macros pour implémenter cette fonctionnalité, la nécessité de réutiliser la fonction ou le champ dans la macro.