Utilisation inhabituelle du fichier .h dans C

Lors de la lecture d’un article sur le filtrage, j’ai trouvé une étrange utilisation du fichier .h – utilisez-le pour remplir un tableau de coefficients:

 #define N 100 // filter order float h[N] = { #include "f1.h" }; //insert coefficients of filter float x[N]; float y[N]; short my_FIR(short sample_data) { float result = 0; for ( int i = N - 2 ; i >= 0 ; i-- ) { x[i + 1] = x[i]; y[i + 1] = y[i]; } x[0] = (float)sample_data; for (int k = 0; k < N; k++) { result = result + x[k]*h[k]; } y[0] = result; return ((short)result); } 

Donc, est-il normal d’utiliser float h[N] = { #include "f1.h" }; par ici?

Les directives de préprocesseur comme #include ne font que des substitutions textuelles (voir la documentation de GNU cpp dans GCC ). Cela peut se produire n’importe où (en dehors des commentaires et des littéraux de chaîne).

Cependant, un #include devrait avoir son # comme premier caractère non vide de sa ligne. Vous allez donc coder

 float h[N] = { #include "f1.h" }; 

La question initiale ne comportait pas #include sur sa propre ligne, donc le code était erroné.

Ce n’est pas une pratique normale , mais c’est une pratique autorisée . Dans ce cas, je suggère d’utiliser une autre extension que .h par exemple, utilisez #include "f1.def" ou #include "f1.data"

Demandez à votre compilateur de vous montrer le formulaire pré-traité. Avec GCC comstackr avec gcc -C -E -Wall yoursource.c > yoursource.i et regarder avec un éditeur ou un pager dans le yoursource.i généré

Je préfère en fait avoir de telles données dans son propre fichier source. Je suggère donc plutôt de générer un fichier h-data.c en utilisant, par exemple, un outil comme GNU awk (le fichier h-data.c commencerait par const float h[345] = { et se terminerait par }; .. .) Et si c’est une donnée constante, mieux le déclarer const float h[] (il pourrait donc se .rodata dans un segment en lecture seule tel que .rodata sous Linux). De plus, si les données incorporées sont volumineuses, le compilateur peut prendre du temps pour l’optimiser (vous pouvez alors comstackr rapidement votre h-data.c sans optimisations).

Alors, est-il normal d’utiliser float h [N] = {#include “f1.h”}; par ici?

Ce n’est pas normal, mais c’est valide (sera accepté par le compilateur).

Avantages d’utiliser ceci: cela vous épargne le peu d’effort nécessaire pour penser à une meilleure solution.

Désavantages:

  • cela augmente le rapport WTF / SLOC de votre code.
  • il introduit une syntaxe inhabituelle, à la fois dans le code client et dans le code inclus.
  • Pour comprendre ce que fait f1.h, vous devez regarder comment il est utilisé (cela signifie que vous devez append des documents supplémentaires à votre projet pour expliquer cette bête, ou que les gens devront lire le code pour voir ce qu’il signifie – aucune solution n’est acceptable)

C’est l’un de ces cas où 20 minutes supplémentaires passées à réfléchir avant d’écrire le code peuvent vous épargner quelques dizaines d’heures de code et de développeurs pendant toute la durée du projet.

Comme déjà expliqué dans les réponses précédentes, ce n’est pas une pratique normale mais c’est une pratique valide.

Voici une solution alternative:

Fichier f1.h:

 #ifndef F1_H #define F1_H #define F1_ARRAY \ { \ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \ 10,11,12,13,14,15,16,17,18,19, \ 20,21,22,23,24,25,26,27,28,29, \ 30,31,32,33,34,35,36,37,38,39, \ 40,41,42,43,44,45,46,47,48,49, \ 50,51,52,53,54,55,56,57,58,59, \ 60,61,62,63,64,65,66,67,68,69, \ 70,71,72,73,74,75,76,77,78,79, \ 80,81,82,83,84,85,86,87,88,89, \ 90,91,92,93,94,95,96,97,98,99 \ } // Values above used as an example #endif 

Fichier f1.c:

 #include "f1.h" float h[] = F1_ARRAY; #define N (sizeof(h)/sizeof(*h)) ... 

Non, ce n’est pas une pratique normale.

L’utilisation directe d’ un tel format présente peu d’avantages, mais les données pourraient être générées dans un fichier source distinct, ou une définition complète pourrait être créée dans ce cas.


Il y a cependant un “pattern” qui implique d’inclure un fichier dans des endroits aussi aléatoires: X-Macros , tels que ceux-là .

L’utilisation de X-macro consiste à définir une collection une seule fois et à l’utiliser à différents endroits. La définition unique assurant la cohérence de l’ensemble. Comme exemple sortingvial, considérez:

 // def.inc MYPROJECT_DEF_MACRO(Error, Red, 0xff0000) MYPROJECT_DEF_MACRO(Warning, Orange, 0xffa500) MYPROJECT_DEF_MACRO(Correct, Green, 0x7fff00) 

qui peut maintenant être utilisé de plusieurs manières:

 // MessageCategory.hpp #ifndef MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED #define MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED namespace myproject { enum class MessageCategory { # define MYPROJECT_DEF_MACRO(Name_, dummy0_, dummy1_) Name_, # include "def.inc" # undef MYPROJECT_DEF_MACRO NumberOfMessageCategories }; // enum class MessageCategory enum class MessageColor { # define MYPROJECT_DEF_MACRO(dumm0_, Color_, dummy1_) Color_, # include "def.inc" # undef MYPROJECT_DEF_MACRO NumberOfMessageColors }; // enum class MessageColor MessageColor getAssociatedColorName(MessageCategory category); RGBColor getAssociatedColorCode(MessageCategory category); } // namespace myproject #endif // MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED 

Il y a longtemps, les gens ont surexploité le préprocesseur. Voir par exemple le format de fichier XPM qui a été conçu pour que les gens puissent:

 #include "myimage.xpm" 

dans leur code C.

Ce n’est plus considéré comme bon.

Le code de l’OP ressemble à C alors je vais parler de C

Pourquoi est-il abusé du préprocesseur?

La directive de préprocesseur #include est destinée à inclure le code source. Dans ce cas et dans le cas de l’OP, il ne s’agit pas de code source réel mais de données .

Pourquoi est-ce considéré comme mauvais?

Parce que c’est très rigide . Vous ne pouvez pas changer l’image sans recomstackr toute l’application. Vous ne pouvez même pas inclure deux images portant le même nom car cela produira du code non compilable. Dans le cas de l’OP, il ne peut pas modifier les données sans recomstackr l’application.

Un autre problème est qu’il crée un lien étroit entre les données et le code source , par exemple le fichier de données doit contenir au moins le nombre de valeurs spécifiées par la macro N définie dans le fichier de code source.

Le couplage serré impose également un format à vos données. Par exemple, si vous souhaitez stocker des valeurs de masortingce 10×10, vous pouvez choisir d’utiliser un tableau de dimension unique ou un tableau à deux dimensions dans votre code source. Le passage d’un format à l’autre imposera une modification de votre fichier de données.

Ce problème de chargement des données est facilement résolu en utilisant les fonctions d’E / S standard. Si vous devez vraiment inclure des images par défaut, vous pouvez donner un chemin par défaut aux images dans votre code source. Cela permettra au moins à l’utilisateur de modifier cette valeur (via une option #define ou -D au moment de la compilation) ou de mettre à jour le fichier image sans avoir à recomstackr.

Dans le cas de l’OP, son code serait plus réutilisable si les coefficients FIR et les vecteurs x, y étaient transmis en tant qu’arguments. Vous pouvez créer une struct pour regrouper ces valeurs. Le code ne serait pas inefficace et deviendrait réutilisable même avec d’autres coeficients. Les coeficients peuvent être chargés au démarrage à partir d’un fichier par défaut à moins que l’utilisateur ne passe un paramètre de ligne de commande remplaçant le chemin du fichier. Cela supprime le besoin de variables globales et rend les intentions du programmeur explicites. Vous pouvez même utiliser la même fonction FIR dans deux threads, à condition que chaque thread ait sa propre struct .

Quand est-ce acceptable?

Lorsque vous ne pouvez pas effectuer un chargement dynamic des données. Dans ce cas, vous devez charger vos données de manière statique et vous devez utiliser de telles techniques.

Notons que le fait de ne pas avoir access aux fichiers signifie que vous programmez pour une plateforme très limitée et que vous devez donc faire des compromis. Ce sera le cas si votre code fonctionne sur un micro-contrôleur par exemple.

Mais même dans ce cas, je préférerais créer un véritable fichier source C au lieu d’inclure des valeurs à virgule flottante à partir d’un fichier semi-formaté.

Par exemple, fournir une vraie fonction C renvoyant les coefficients, plutôt que d’avoir un fichier de données semi-formaté. Cette fonction C pourrait alors être définie dans deux fichiers différents, l’un utilisant des E / S à des fins de développement, et l’autre renvoyant des données statiques pour la version de publication. Vous comstackrez la condition correcte du fichier source.

Il y a parfois des situations qui nécessitent l’utilisation d’outils externes pour générer des fichiers .C basés sur d’autres fichiers contenant du code source, des outils externes générant des fichiers C avec une quantité excessive de code câblé dans les outils de génération ou #include directive de diverses manières “inhabituelles”. Parmi ces approches, je suggérerais que ce dernier – bien que icky – soit souvent le moins mauvais.

Je suggère d’éviter l’utilisation du suffixe .h pour les fichiers qui ne respectent pas les conventions normales associées aux fichiers d’en-tête (par exemple en incluant des définitions de méthodes, en allouant de l’espace, en exigeant un contexte d’inclusion inhabituel) nécessitant une inclusion multiple avec des macros différentes définies, etc. Je évite aussi généralement d’utiliser .c ou .cpp pour les fichiers incorporés dans d’autres fichiers via #include sauf si ces fichiers sont principalement utilisés de manière autonome [dans certains cas, par exemple, un fichier fooDebug.c contenant #define SPECIAL_FOO_DEBUG_VERSION [ #define SPECIAL_FOO_DEBUG_VERSION ] `#include” foo.c “` `si je souhaite avoir deux fichiers d’object avec des noms différents générés à partir de la même source, et l’un d’eux est la version” normale “.]

Ma pratique normale consiste à utiliser .i comme suffixe pour les fichiers générés par l’homme ou ceux générés par une machine et conçus pour être inclus, mais de manière habituelle, à partir d’autres fichiers source C ou C ++. Si les fichiers sont générés par une machine, je demanderai généralement à l’outil de génération d’inclure en première ligne un commentaire identifiant l’outil utilisé pour le créer.

BTW, une astuce que j’ai utilisée était que je voulais permettre à un programme d’être construit en utilisant juste un fichier de commandes, sans aucun outil tiers, mais que je voulais compter combien de fois il a été construit. Dans mon fichier de commandes, j’ai inclus echo +1 >> vercount.i ; puis dans le fichier vercount.c, si je me souviens bien:

 const int build_count = 0 #include "vercount.i" ; 

L’effet net est que j’obtiens une valeur qui s’incrémente à chaque génération sans avoir à recourir à des outils tiers pour la produire.

Comme déjà dit dans les commentaires, ceci n’est pas une pratique normale. Si je vois un tel code, j’essaie de le refactoriser.

Par exemple f1.h pourrait ressembler à ceci

 #ifndef _f1_h_ #define _f1_h_ #ifdef N float h[N] = { // content ... } #endif // N #endif // _f1_h_ 

Et le fichier .c:

 #define N 100 // filter order #include “f1.h” float x[N]; float y[N]; // ... 

Cela me semble un peu plus normal – même si le code ci-dessus pourrait encore être amélioré (en éliminant les globales par exemple).

Ajout à ce que tout le monde a dit – le contenu de f1.h doit être comme ceci:

 20.0f, 40.2f, 100f, 12.40f -122, 0 

Parce que le texte dans f1.h va initialiser le tableau en question!

Oui, il peut avoir des commentaires, une autre fonction ou une utilisation de macro, des expressions, etc.

C’est une pratique normale pour moi.

Le préprocesseur vous permet de diviser un fichier source en autant de morceaux que vous le souhaitez, assemblés par les directives #include.

Cela est très logique lorsque vous ne voulez pas encombrer le code avec des sections longues / à ne pas lire telles que les initialisations de données. En fin de compte, mon fichier d’initialisation du tableau a une longueur de 11 000 lignes.

Je les utilise également lorsque certaines parties du code sont générées automatiquement par un outil externe: il est très pratique de faire en sorte que l’outil génère juste ses morceaux et de les inclure dans le rest du code écrit à la main.

J’ai certaines de ces inclusions pour certaines fonctions qui ont plusieurs implémentations alternatives en fonction du processeur, certaines utilisant l’assemblage en ligne. Les inclusions rendent le code plus facile à gérer.

Par tradition, la directive #include a été utilisée pour inclure des fichiers d’en-tête, c’est-à-dire des ensembles de déclarations exposant une API. Mais rien ne le prescrit.

Lorsque le préprocesseur trouve la directive #include il ouvre simplement le fichier spécifié et en insère le contenu, comme si le contenu du fichier avait été écrit à l’emplacement de la directive.

Je lis que les gens veulent refactoriser et dire que c’est mal. J’ai quand même utilisé dans certains cas. Comme certaines personnes l’ont dit, il s’agit d’une directive pré-procédé, de même que du contenu du fichier. Voici un cas où j’ai utilisé: la construction de nombres aléatoires. Je construis des nombres aléatoires et je ne veux pas le faire chaque fois que je comstack ni en exécution. Donc, un autre programme (généralement un script) remplit simplement un fichier avec les nombres générés qui sont inclus. Cela évite la copie à la main, cela permet de changer facilement les nombres, l’algorithme qui les génère et d’autres subtilités. Vous ne pouvez pas blâmer facilement la pratique, dans ce cas, c’est simplement la bonne façon.

J’ai utilisé la technique de l’OP consistant à placer un fichier d’inclusion pour la partie initialisation des données d’une déclaration de variable pendant un certain temps. Tout comme l’OP, le fichier inclus a été généré.

J’ai isolé les fichiers .h générés dans un dossier distinct pour les identifier facilement:

 #include "gensrc/myfile.h" 

Ce schéma s’est effondré lorsque j’ai commencé à utiliser Eclipse. La vérification de la syntaxe Eclipse n’était pas assez sophistiquée pour gérer cela. Il réagirait en signalant des erreurs de syntaxe là où il n’y en avait pas.

J’ai signalé des échantillons à la liste de diffusion Eclipse, mais il ne semblait pas y avoir beaucoup d’intérêt à “corriger” la vérification de la syntaxe.

J’ai changé mon générateur de code pour prendre des arguments supplémentaires afin de générer toute la déclaration de variable, pas seulement les données. Maintenant, il génère des fichiers include syntaxiquement corrects.

Même si je n’utilisais pas Eclipse, je pense que c’est une meilleure solution.

Dans le kernel Linux, j’ai trouvé un exemple, IMO, magnifique. Si vous regardez le fichier d’en-tête cgroup.h

http://lxr.free-electrons.com/source/include/linux/cgroup.h

vous pouvez trouver la directive #include utilisée deux fois, après différentes définitions de la macro SUBSYS(_x) ; cette macro est utilisée dans cgroup_subsys.h, pour déclarer plusieurs noms de groupes de contrôle Linux (si vous n’êtes pas familier avec les groupes de contrôle, ce sont des interfaces conviviales pour Linux, qui doivent être initialisées au démarrage du système).

Dans l’extrait de code

 #define SUBSYS(_x) _x ## _cgrp_id, enum cgroup_subsys_id { #include &ltlinux/cgroup_subsys.h&gt CGROUP_SUBSYS_COUNT, }; #undef SUBSYS 

chaque SUBSYS(_x) déclaré dans cgroup_subsys.h devient un élément du type enum cgroup_subsys_id , alors que dans l’extrait de code

 #define SUBSYS(_x) extern struct cgroup_subsys _x ## _cgrp_subsys; #include &ltlinux/cgroup_subsys.h&gt #undef SUBSYS 

chaque SUBSYS(_x) devient la déclaration d’une variable de type struct cgroup_subsys .

De cette façon, les programmeurs du kernel peuvent append des groupes de contrôle en modifiant uniquement cgroup_subsys.h, tandis que le pré-processeur appenda automatiquement les valeurs / déclarations d’énumération associées dans les fichiers d’initialisation.