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 depuis struct A est le premier membre de struct B, donc b->x serait le même que b->a.x et offre une meilleure lisibilité.
Cela a du sens, mais est-ce considéré comme une bonne pratique? Et cela fonctionnera-t-il sur toutes les plateformes? Actuellement, cela fonctionne bien sur GCC.

60
demandé sur Grijesh Chauhan 2014-03-06 12:10:08

11 réponses

Oui, cela fonctionnera multi-plateforme, mais cela ne fait pas nécessairement une bonne idée.

Selon la norme ISO C (toutes les citations ci-dessous proviennent 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

En outre, 6.2.7 Compatible type and composite type indique que:

Deux types ont un type compatible si leurs types sont les mêmes

Et il est incontestable que les types A et A-within-B sont identique.

Cela signifie que l'accès en mémoire aux champs A sera le même dans les types A et B, comme le serait le plus sensible b->a.x qui est probablement ce que vous devriez utiliser si vous avez des préoccupations concernant la maintenabilité à l'avenir.

Et, bien que vous ayez normalement à vous soucier de l'aliasing de type strict, Je ne crois pas que cela s'applique ici. Il est illégal d'alias de pointeurs mais la norme a des exception.

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 objet peut ou non être alias.

Les exceptions énumérées sont:

  • a type compatible with the effective type of the object;
  • quelques 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 structure mentionnées ci-dessus, y compris la phrase:

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

Semble indiquer que cet exemple est spécifiquement autorisé. Le point central, nous avons à retenir ici est que le type de l'expression ((A*)b) est A*, pas B*. Cela rend les variables compatibles aux fins d'aliasing sans restriction.

C'est ma lecture des parties pertinentes de la norme, j'ai eu tort avant de (a), mais j'en doute dans ce cas.

Donc, si vous avez un véritable besoin pour cela, cela fonctionnera bien mais je documenterais toutes les contraintes dans le code très proche des structures afin de ne pas être mordu à l'avenir.


(a) comme ma femme vous le dira, fréquemment et sans trop d'incitation: -)

62
répondu paxdiablo 2014-03-08 23:09:23

Je vais aller sur une branche et opposer @ paxdiablo sur celui-ci: je pense que c'est une bonne idée, et c'est très commun dans le grand code de qualité de production.

C'est fondamentalement la façon la plus évidente et la plus agréable d'implémenter des structures de données orientées objet basées sur l'héritage en C. démarrer la déclaration de struct B avec une instance de struct A signifie "B est une sous-classe de A". Le fait que le premier membre de la structure soit garanti 0 octet depuis le début de la structure est ce qui le fait fonctionner en toute sécurité, et il est borderline belle à mon avis.

Il est largement utilisé et déployé dans le code basé sur la bibliothèqueGObject , telle que la boîte à outils 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 lors de l'implémentation de relations de type compliquées dans C.:)

Dans le cas de GObject et GTK+, il y a beaucoup d'infrastructure de support et de documentation pour vous aider ceci: il est assez difficile de l'oublier. Cela pourrait signifier que la création d'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 en C pour les classes.

33
répondu unwind 2017-05-23 12:09:26

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 la distribution ni cet ordre ne peuvent être appliqués par le compilateur.

Cela devrait fonctionner multi-plateforme, mais je ne pense pas que ce soit une bonne pratique.

Si vous avez vraiment des structures profondément imbriquées (vous devrez peut-être vous demander pourquoi), alors vous devriez utiliser une variable locale temporaire pour accéder aux Champs:

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;

Ou, si vous ne voulez pas copier a le long de:

A* deep_a = &(e->d.c.b.a);
deep_a->x = 10;
deep_a->y = deep_a->x + 72;

Cela montre d'où vient a et il ne nécessite pas de distribution.

Java et C # exposent également régulièrement des constructions comme "C. B. a", Je ne vois pas quel est le problème. Si ce que vous voulez simuler est un comportement orienté objet, alors vous devriez envisager d'utiliser un langage orienté objet (comme C++), puisque "étendre les structures" de la manière que vous proposez ne fournit pas d'encapsulation ni de polymorphisme d'exécution (bien que l'on puisse soutenir que ((A*)b) s'apparente à un "dynamique jeter").

12
répondu dureuill 2014-03-07 07:45:20

C'est une horrible idée. Dès que quelqu'un arrive et insère un autre champ à l'avant de la structure B, Votre programme explose. Et quel est le problème avec b.a.x?

11
répondu jaket 2014-03-06 08:15:11

Je suis désolé de ne pas être d'accord 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, Ceci est appelé aliasing et n'est pas autorisé par les règles strictes d'aliasing Un moins laid était de faire cela serait d'utiliser des fonctions de getter en ligne qui n'ont alors pas à avoir l'air soigné de cette façon. Ou peut-être que c'est le travail d'un syndicat? Spécifiquement autorisé à tenir l'un de plusieurs types, mais il y a une myriade d'autres inconvénients là aussi.

En bref, ce genre de coulée sale pour créer du polymorphisme n'est pas autorisé par la plupart des normes C, juste 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 du code qui ne suit pas ces règles http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization

7
répondu Vality 2014-03-06 14:08:06

Oui, ça va marcher. Et c'est l'un des principes de base de orienté objet en utilisant C. Voir cette réponse ' Object-orientation in C ' pour plus d'exemples sur l'extension (c'est-à-dire l'héritage).

5
répondu ItsMe 2017-05-23 12:09:26

C'est parfaitement légal, et, à mon avis, assez élégant. Pour un exemple de ceci dans le code de production, consultez les documents GObject :

Grâce à ces conditions simples, il est possible de détecter le type de chaque instance d'objet en faisant:

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 ont tendance à conduire à d'énormes switch déclarations, ce qui est une grande partie de ce que vous avez travaillé pour éviter en écrivant du code OO. Je écrivez moi-même une quantité importante de code dans ce style - - - typiquement, le premier membre du struct contient des pointeurs de fonction qui peuvent fonctionner comme une vtable pour le type en question.

3
répondu Patrick Collins 2014-03-07 07:53:20

Je peux voir comment cela fonctionne mais je n'appellerais pas cette bonne pratique. Cela dépend de la façon dont les octets de chaque structure de données sont placés en mémoire. Chaque fois que vous lancez une structure de données compliquée à une autre (ie. structs), ce n'est pas une très bonne idée, surtout quand les deux structures ne sont pas de la même taille.

2
répondu stakSmashr 2014-03-06 08:17:04

Je pense que L'OP et de nombreux commentateurs se sont accrochés à l'idée que le code étend une structure.

Ce n'est pas le cas.

Ceci 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 bien sûr 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];

};

Alors maintenant, vous avez une deuxième structure qui peut être traitée (la succession) exactement comme la première avec un distribution explicite.

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

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

Je ne suggérerais pas d'écrire des structures comme ça juste pour la lisibilité.

1
répondu Joshua Clayton 2014-03-13 23:57:06

Je pense que Postgres le fait aussi dans certains de leurs codes. Non pas que cela en fait une bonne idée, mais cela dit quelque chose sur la façon dont il semble être largement accepté.

0
répondu Christian Convey 2014-03-11 19:39:18

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

-1
répondu jiajia jiang 2017-08-06 16:34:17