Type-sécurité générique des structures de données dans la plaine, vieux C?
J'ai fait beaucoup plus de la programmation en C++ de "plain old C" de la programmation. Une chose que j'ai cruellement manquer lors de la programmation dans la plaine, C est de type sécurisé générique des structures de données, qui sont fournis en C++ à l'aide de modèles.
Pour des raisons de concret, considérons un générique seule liste liée. En C++, c'est une simple question de définir votre propre modèle de classe, puis l'instancier pour les types dont vous avez besoin.
En C, je ne peux penser à quelques façons de mettre en œuvre un générique individuellement liée liste:
- Écrire la liste type(s) et des procédures à l'appui une fois, à l'aide de pointeurs void pour contourner le système de type.
- Écrire des macros du préprocesseur prendre les noms de type, etc, afin de générer un type spécifique de la version de la structure de données et des procédures de soutien.
- Utiliser une version plus sophistiquée, outil autonome pour générer le code pour les types dont vous avez besoin.
Je n'aime pas l'option 1, car il est subvertit le système de type, et serait susceptible d'avoir de moins bonnes performances que un type spécialisé de mise en œuvre. À l'aide d'une représentation unifiée de la structure de données pour tous les types, et le moulage à/de void pointeurs, autant que je peux voir, nécessite une indirection qui pourraient être évitées par une mise en œuvre spécialisé pour le type d'élément.
L'Option 2 ne nécessite aucun outil supplémentaire, mais il se sent un peu maladroit, et pourrait donner de mauvaises erreurs du compilateur lorsque mal utilisés.
Option 3 pourrait donner de meilleurs messages d'erreur du compilateur que l'option 2, en tant qu'institution spécialisée de données de code de structure réside dans la forme étendue qui peut être ouvert dans un éditeur et inspectés par le programmeur (par opposition au code généré par le préprocesseur macros). Toutefois, cette option est la plus lourds, une sorte de "du pauvre modèles". J'ai utilisé cette méthode avant de, à l'aide d'un simple script sed à se spécialiser un "basé sur un modèle" version de code C.
J'aimerais programme de mon avenir "bas niveau" des projets en C plutôt qu'en C++, mais ont été effrayée à l'idée de réécrire les structures de données communes pour chaque type spécifique.
Quelle est l'expérience des gens avec ce problème? Sont de bonnes bibliothèques du générique de structures de données et algorithmes en C qui ne vont pas avec l'Option 1 (c'est à dire la conversion et de void pointeurs, qui sacrifie la sécurité du type et ajoute un niveau d'indirection)?
- Vous avez manqué 4) mettre en Œuvre pleinement polymorphe de l'objet système en c. Ce qui est beaucoup plus d'ennuis que cela vaut la peine.
- Après avoir fait la méthode 2 quelques fois, je pense que la méthode 3 serait la façon dont je vais m'attaquer la prochaine fois. Vous pouvez mettre en œuvre 3 avec le même effort que 2, tout en obtenant de meilleurs messages d'erreur.
- Entièrement polymorphe de l'objet système en C serait un énorme effort, et, si l'on étendait un compilateur C (plutôt que la mise en œuvre de l'objet système de bibliothèque), vous manquez sur toutes sortes de statique des garanties qui pourraient être fournis par un langage conçu avec polymorphes systèmes d'objets à l'esprit. Auquel cas: pourquoi ne pas simplement utiliser le C++? Je veux une solution simple.
- J'ai modifié le post de souligner que je veux de sécurité de type, ce qui n'est pas fourni par l'Option 1, et je veux éviter l'indirection supplémentaire que l'utilisation d'une représentation unique pour tous les types nécessiterait.
- Bâtiment OO en c est un grand projet, mais pas aussi grand que ça sonne (le dillo gens du projet a fait un travail décent dans une très petite quantité de code pour la v1.la série x avant de passer à c++ pour la v2.la série x). C'est juste que si vous avez besoin de toutes ces belles garantit que vous êtes presque certainement mieux à l'aide de c++ ou D ou objective-c, ou quelque chose qui a déjà eux.
- ou il y a GObject...
- pour la mention D 🙂
- OpenGC3 [ voir
ccxll(T)
] est-ce que vous cherchez! Cependant, il n'est pas encore terminée, et que deux sortes de listes liées mises en œuvre, mais il est suffisant pour démontrer comment construire type-sécurité générique des structures de données dans la plaine, vieux C avec la deuxième option mentionnés ci-dessus.
Vous devez vous connecter pour publier un commentaire.
L'Option 1 est l'approche adoptée par la plupart des implémentations C du générique de conteneurs que je vois. Windows driver kit et le noyau Linux et utiliser une macro pour permettre des liens pour les conteneurs être intégré n'importe où dans une structure, avec la macro utilisée pour obtenir le pointeur de structure à partir d'un pointeur vers le champ lien:
list_entry()
macro dans LinuxCONTAINING_RECORD()
macro dans WindowsL'Option 2 est le point d'amure prises par BSD de l'arbre.h et la file d'attente.h récipient de mise en œuvre:
Je ne pense pas que je voudrais examiner l'une de ces approches de type sécuritaire. Utile, mais pas de type coffre-fort.
list.h
approche le mieux.C est un autre genre de beauté que C++, et le type de sécurité et d'être toujours en mesure de voir ce que tout est tracé à l'aide de code sans impliquer jette dans votre débogueur n'est généralement pas l'un d'eux.
C est la beauté vient beaucoup de son manque de sécurité de type, de contourner le système de type et à la crue au niveau des bits et des octets. En raison de cela, il y a certaines choses qu'il peut faire plus facilement sans lutter contre la langue, comme, par exemple, de longueur variable, aux structures, à l'aide de la pile de même pour les tableaux dont les dimensions sont déterminées au moment de l'exécution, etc. Il a aussi tendance à être beaucoup plus simple de préserver ABI lorsque vous travaillez à ce faible niveau.
Donc, il y a un type différent de l'esthétique impliqué ici, ainsi que les différents défis, et je le recommande un changement dans l'état d'esprit lorsque vous travaillez dans C. Pour vraiment l'apprécier, je vous suggère de faire des choses beaucoup de gens prennent pour acquis ces jours-ci, à l'instar de la mise en œuvre de votre propre allocateur de mémoire ou de pilote de périphérique. Lorsque vous travaillez à un niveau si faible, vous ne pouvez pas aider mais regarder tout ce que la mémoire des schémas de bits et d'octets par opposition aux "objets", avec des comportements attachés. En outre, il peut venir un moment dans ce bas-niveau bit/octet code de manipulation où C devient plus facile de comprendre que le code C++ jonché de
reinterpret_casts
, par exempleComme pour votre liste chaînée exemple, je vous suggère un non-intrusive version de nœud (qui ne nécessite pas de stockage de liste de pointeurs dans le type d'élément, les
T
, lui-même, permettant la liste liée de la logique et de la représentation pour être découplée deT
lui-même), comme suit:Maintenant, nous pouvons créer une liste de nœud comme suit:
Pour récupérer l'élément de la liste comme T*:
Puisque c'est C, il n'y a pas de vérification de type que ce soit lors de la coulée des pointeurs de cette façon, et qui sera probablement également vous donner un sentiment de malaise si vous êtes en provenance d'un C++ arrière-plan.
La partie la plus délicate est de faire en sorte que ce membre,
element
, est correctement aligné quel que soit le type que vous souhaitez stocker. Quand vous pouvez résoudre ce problème de façon portable comme vous le souhaitez, vous aurez une solution puissante pour la création efficace de la mémoire et des mises allocateurs. Souvent, cela vous permettra de vous juste à l'aide de max alignement pour tout ce qui peut paraître aberrant, mais généralement ce n'est pas si vous êtes en utilisant des structures de données et les allocateurs qui ne sont pas à payer ces frais pour de nombreux petits éléments sur une base individuelle.Maintenant cette solution implique la conversion de type. Il y a peu de choses que vous pouvez faire à ce sujet à moins d'avoir une version distincte de code de cette liste de nœud et de la logique correspondante de travailler avec elle pour chaque type, T, que vous souhaitez soutenir (à court de dynamique polymorphisme). Cependant, elle n'implique pas un niveau supplémentaire d'indirection que vous pourriez avoir pensé était nécessaire, et encore alloue la totalité de la liste de nœud et de l'élément en une allocation unique.
Et je recommanderais cette façon simple d'obtenir un genericity dans C dans beaucoup de cas. Il suffit de remplacer
T
avec un tampon a une longueur correspondantsizeof(T)
et alignés correctement. Si vous avez un raisonnablement portable de manière sécurisée et vous pouvez généraliser pour assurer un alignement parfait, que vous aurez un très puissant moyen de travailler avec la mémoire d'une manière qui améliore souvent le cache, réduit la fréquence des allocations de tas/deallocations, le montant de l'indirection nécessaire, les temps de construire, etc.Si vous avez besoin de plus d'automatisation comme avoir
list_new_node
initialiser automatiquementstruct Foo
, je recommande la création d'un général table de type struct que vous pouvez passer autour de laquelle contient des informations comme la façon dont big T est un pointeur de fonction qui pointe vers une fonction pour créer une instance par défaut de T, une autre pour copier T, clone T, détruire T, un comparateur, etc. En C++, vous pouvez générer ce tableau automatiquement à l'aide de modèles et intégré dans la langue des concepts comme la copie des constructeurs et des destructeurs. C nécessite un peu plus d'effort manuel, mais vous pouvez encore réduire le passe-partout un peu avec les macros.Un autre truc qui peut être utile si vous allez avec un plus macro-orientée vers la génération de code de la route est de l'argent dans un préfixe ou un suffixe à base de convention de nommage des identifiants. Par exemple, CLONE(Type ptr) pourrait être défini pour revenir
Type##Clone(ptr)
, doncCLONE(Foo, foo)
peut invoquerFooClone(foo)
. C'est une sorte de tricher pour obtenir quelque chose qui ressemble à de surcharge de fonctions en C, et est utile lors de la génération de code en vrac (quand le CLONE est utilisé pour mettre en œuvre une autre macro) ou même un peu de la copie et collage de texte standard-type de code pour au moins améliorer l'uniformité de la réutilisable.L'Option 1, soit à l'aide de
void *
ou certainsunion
en fonction de la variante est ce que la plupart des programmes en C utiliser, et il peut vous donner de MEILLEURES performances que le C++/macro style d'avoir plusieurs implémentations pour les différents types, car il y a moins duplication de code, et donc de moins en moins icache de pression et moins d'icache manque.GLib est a un tas de données générique structures, http://www.gtk.org/
CCAN a un tas de utile des extraits et ces http://ccan.ozlabs.org/
Votre option 1 est ce que la plupart des temps anciens programmeurs c irait pour, éventuellement, salé, avec un peu de 2 de couper vers le bas sur le côté répétitif de frappe, et juste peut-être employant quelques pointeurs de fonction pour une saveur de polymorphisme.
Il y a une variation fréquente de l'option 1, qui est plus efficace, car elle utilise des syndicats pour stocker les valeurs dans la liste des noeuds, c'est à dire il n'y a aucun indirection. Cela a l'inconvénient que la liste accepte uniquement les valeurs de certains types et potentiellement les déchets de la mémoire si les types sont de tailles différentes.
Cependant, il est possible de se débarrasser de la
union
par la flexibilité des membres de la pile de la place si vous êtes prêt à casser la stricte aliasing. C99 exemple de code:Une vieille question, je sais, mais dans ce cas il est toujours d'intérêt: j'ai essayé avec l'option 2) (pré-processeur de macros) aujourd'hui, et est venu avec l'exemple que j'ai coller ci-dessous. Légèrement maladroit, certes, mais pas terrible. Le code n'est pas entièrement sûr, mais contient des contrôles d'intégrité de fournir un niveau de sécurité raisonnable. Et de traiter avec les messages d'erreur du compilateur lors de l'écriture il est doux par rapport à ce que j'ai vu lorsque les modèles C++ est entré en jeu. Vous êtes probablement mieux de commencer la lecture de cet exemple utiliser le code dans le "principal" de la fonction.
- Je utiliser des pointeurs void (void*) pour représenter les structures de données générique défini avec les structures et les typedefs. Ci-dessous je partage ma mise en œuvre d'une lib qui je travaille.
Avec ce type de mise en œuvre, vous pouvez penser de chaque nouveau type, défini avec la définition de type, comme une pseudo-classe. Ici, cette pseudo-classe est l'ensemble du code source (some_type_implementation.c) et son fichier d'en-tête (some_type_implementation.h).
Dans le code source, vous devez définir la structure qui présentera le nouveau type. Remarque la structure dans le "nœud.c" fichier source. Là, j'ai fait un vide pointeur vers le "info" attribut. Ce pointeur peut transporter n'importe quel type de pointeur (je pense), mais le prix que vous devez payer est un identificateur de type à l'intérieur de la structure (de type int), et tous les switchs pour faire de la propper poignée de chaque type défini. Ainsi, dans le nœud.h" fichier d'en-tête, j'ai défini le type "Nœud" (juste pour éviter d'avoir à type struct noeud à chaque fois), et aussi, j'ai eu à définir les constantes "EMPTY_NODE", "COMPLEX_NODE", et "MATRIX_NODE".
Vous pouvez effectuer la compilation, à la main, avec "gcc *.c -lm".
principal.Fichier Source c
nœud.Fichier Source c
nœud.h Fichier d'en-Tête
de la matrice.Fichier Source c
de la matrice.h Fichier d'en-Tête
complexe.Fichier Source c
complexe.h Fichier d'en-Tête
Je l'espère, je n'avais pas manqué de rien.
Je suis à l'aide de l'option 2 pour une paire de haute performance des collections, et il est extrêmement consommatrice de temps de travail par le biais de la quantité de macro logique nécessaire pour faire quelque chose de vraiment le temps de compilation génériques et de la valeur de l'aide. Je suis en train de faire ce purement pour des performances brutes (jeux). Un X-macros approche est utilisée.
Un douloureux problème qui constamment jusqu'à l'Option 2 est, "en Supposant un nombre fini d'options, telles que 8/16/32/64 peu touches, puis-je faire, dit de la valeur d'une constante et de définir plusieurs fonctions de chaque élément de cet ensemble de valeurs de cette constante peut prendre sur, ou dois-je simplement faire une variable de membre?" L'ancien signifie moins performant cache d'instructions, car vous avez beaucoup de la répétition des fonctions avec un ou deux numéros différents, tandis que le second signifie que vous avez de référence attribué variables qui, dans le pire des cas, signifie un cache de données manquer. Étant donné que l'Option 1 est purement dynamique, vous fera les valeurs des variables de membre sans même y penser. C'est vraiment de la micro-optimisation, cependant.
Également garder à l'esprit le trade-off entre le retour des pointeurs vs valeurs: ce dernier est plus performant lorsque la taille de l'élément de données est inférieure ou égale à la taille du pointeur; que, si l'élément de données est plus grand, il est probablement mieux de retourner pointeurs plutôt que de les forcer une copie d'un objet de grande taille en retournant la valeur.
Je suggère fortement d'aller pour l'Option 1, quel que soit le scénario où vous n'êtes pas certain à 100% que le rendement de la collecte sera le goulot d'étranglement. Même avec mon utilisation de l'Option 2, mes collections de la bibliothèque fournit un "quick setup" qui ressemble à l'Option 1, à savoir l'utilisation de
void *
valeurs dans ma liste et la carte. Cela est suffisant pour 90% de cas.