2 votes

Pourquoi GCC génère-t-il des erreurs et des avertissements inutiles uniquement lors de l'utilisation du nom de la structure au lieu du typedef ?

Je dispose d'un programme composé de deux fichiers source (farm.c, init.c) et de deux fichiers d'en-tête correspondants (farm.h, init.h) Les deux fichiers source contiennent des gardes d'en-tête et chacun requiert des fonctions/variables de l'autre.

init.h:

#ifndef INIT_H
#define INIT_H

#include
#include
#include"farm.h"

#define PIG_SOUND "oink"
#define CALF_SOUND "baa"

enum types {PIG, CALF};

typedef struct resources {
    size_t pork;
    size_t veal;
    size_t lamb;
    size_t milk;
    size_t eggs;
} resources;

typedef struct animal {
    size_t legs;
    char* sound;
    int efficiency;
    void (*exclaim)(struct animal*);
    void (*work)(struct animal*, struct resources*);
} animal;

/* J'ai essayé différentes façons de déclarer des structs en plus du
   typedef comme ceci */

//animal stock;
//animal farm;

void make_pig(struct animal* a, int perf);
void make_calf(struct animal* a, int perf);

#endif

farm.h:

#ifndef FARM_H
#define FARM_H

#include
#include
#include
#include
#include"init.h"

/* GCC ne reconnait pas le typedef ou l'identificateur de la struct 
   jusqu'à ce que ces déclarations avancées aient été faites en
   plus du fichier d'en-tête init.h inclus */

//typedef struct animal animal;
//typedef struct resources resources;

void exclaim(animal* b);
void work(struct animal* b, struct resources* s);

#endif

init.c:

#include"init.h"

void make_pig(animal* a, int perf) {
    a->legs = 4;
    a->sound = PIG_SOUND;
    a->efficiency = perf;
    a->exclaim = exclaim;
    a->work = work;
}

void make_calf(animal* a, int perf) {
    a->legs = 4;
    a->sound = CALF_SOUND;
    a->efficiency = perf;
    a->exclaim = exclaim;
    a->work = work;
}

farm.c:

#include"farm.h"

int main() {
    return 0;
}

void exclaim(animal* a) {
    for (int i = 0; i < 3; i++) {
        printf("%s ", a->sound);
    }
    printf("\n");
}

void work(animal* a, struct resources* r) {
    if (!strcmp(a->sound, PIG_SOUND)) {
        r->pork += a->efficiency;
    }

    if (!strcmp(a->sound, CALF_SOUND)) {
        r->veal += a->efficiency;
    }
}

Utiliser les deux types de noms (c'est-à-dire, struct ani et animal) fonctionne normalement très bien sur mon système Linux avec la norme C99. Cependant, lorsque j'utilise struct ani au lieu de animal ici, je reçois les avertissements ci-dessous pour chaque instance d'utilisation du type pour struct ani et struct resources.

lib/farm.h:10:21: warning: ‘struct ani’ declared inside parameter list will not be visible outside of this definition or declaration
   10 | void exclaim(struct ani* a);
      |                     ^~~
lib/farm.h:11:33: warning: ‘struct resources’ declared inside parameter list will not be visible outside of this definition or declaration
   11 | void work(struct ani* a, struct resources* r);

Et 10 avertissements au total pour chaque utilisation de pointeurs de fonctions de la forme :

src/init.c:17:16: warning: assignment to ‘void (*)(struct ani *)’ from incompatible pointer type ‘void (*)(struct ani *)’ [-Wincompatible-pointer-types]
   17 |     a->exclaim = exclaim;
      |                ^
src/init.c:18:13: warning: assignment to ‘void (*)(struct ani *, struct resources *)’ from incompatible pointer type ‘void (*)(struct ani *, struct resources *)’ [-Wincompatible-pointer-types]
   18 |     a->work = work;

Est-ce que quelqu'un pourrait expliquer pourquoi ce comportement se produit et comment je peux éviter les problèmes ? En général, il me faut un temps incommensurable pour résoudre ces erreurs et je ne comprends pas vraiment quelle est mon erreur au départ.

6voto

rici Points 45980

Vous avez rencontré l'un des cas particuliers des règles de portée de C.

Informellement, une struct étiquetée (ou union, mais je ne vais pas répéter cela encore et encore) prend naissance lorsqu'elle est nommée si aucune déclaration n'est visible pour elle. "Prend naissance" signifie qu'elle est considérée comme déclarée dans la portée actuelle. De plus, si une struct étiquetée a été nommée précédemment dans une portée et que vous déclarez ensuite une struct avec la même étiquette, les deux structs sont considérées comme étant les mêmes. Jusqu'à ce qu'une déclaration pour la struct soit achevée, la struct est considérée comme un type incomplet, mais un pointeur vers un type incomplet est un type complet, vous pouvez donc déclarer un pointeur vers une struct étiquetée avant de compléter réellement la définition de la struct.

La plupart du temps, cela fonctionne très bien avec peu de réflexion. Mais les prototypes de fonctions sont un peu spéciaux, car un prototype de fonction est une portée à lui tout seul. (La portée ne dure que jusqu'à la fin de la déclaration de fonction).

En combinant tout cela, vous rencontrez le problème auquel vous êtes confronté. Vous ne pouvez pas utiliser un pointeur vers une struct étiquetée dans un prototype de fonction à moins que la struct étiquetée n'ait été connue avant l'apparition du prototype de fonction. Si elle a été mentionnée auparavant, même dans une portée externe, l'étiquette est visible et sera donc considérée comme étant la même struct (même si elle est toujours incomplète). Mais si l'étiquette n'était pas précédemment visible, un nouveau type de struct sera créé au sein de la portée du prototype, qui ne sera pas visible après la fin de cette portée (qui est presque immédiatement).

Concrètement, si vous écrivez ce qui suit:

extern struct animal * barnyard;
void exclaim(struct animal*);

alors les deux utilisations de struct animal se réfèrent au même type de struct, qui sera probablement complété plus tard (ou dans une autre unité de traduction).

Mais sans la déclaration extern struct animal * barnyard;, la struct animal nommée dans le prototype de exclaim n'est pas visible précédemment, donc elle est déclarée uniquement dans la portée du prototype, donc ce n'est pas le même type qu'une utilisation ultérieure de struct animal. Si vous aviez mis les déclarations dans l'ordre inverse, vous auriez vu un avertissement du compilateur (en supposant que vous ayez demandé des avertissements de compilation):

void exclaim(struct animal*);
extern struct animal * barnyard;

(Sur godbolt, bénissons son cœur)

Une déclaration typedef se comporte de la même manière que la déclaration extern ci-dessus; le fait que ce soit un alias de type n'est pas important. Ce qui importe, c'est que l'utilisation de struct animal dans la déclaration provoque la création du type, et vous pouvez ensuite l'utiliser librement dans un prototype. C'est la même raison pour laquelle les pointeurs de fonction à l'intérieur de vos définitions de structs sont OK; le début de la définition de la struct a été suffisant pour déclarer l'étiquette, donc le prototype la voit.

En fait, toute construction syntaxique contenant une struct étiquetée (struct whatever) servira le même but, car ce qui compte est l'effet de mentionner une struct étiquetée sans déclaration visible. Ci-dessus, j'ai utilisé des déclarations globales extern comme exemples car ce sont des lignes qui pourraient apparaître dans un en-tête, mais il existe de nombreuses autres possibilités, y compris même la déclaration d'une fonction qui retourne un pointeur vers une struct (car le type de retour d'une déclaration de fonction n'est pas dans la portée du prototype).

Consultez ci-dessous pour quelques commentaires supplémentaires sur la question éditée.


Ma préférence personnelle est d'utiliser toujours des typedefs comme déclarations avancées des étiquettes, et de ne jamais utiliser struct foo n'importe où dans mon code autre que le typedef et la définition ultérieure :

typedef struct Animal Animal;

void exclaim(Animal*);

// ...

// Plus tard ou dans un en-tête différent
struct Animal {
  Animal* next;
  void (*exclaim)(Animal *);
  // etc.
};

Remarquez que j'utilise toujours le même identifiant pour l'étiquette et le typedef. Pourquoi pas? Il n'y a pas de confusion et les étiquettes ne sont pas dans le même espace de noms que les autres identifiants depuis que C était primitif.

Pour moi, un grand avantage de ce style est qu'il me permet de séparer les détails d'implémentation ; l'en-tête public ne contient que les déclarations de typedefs (et les prototypes qui utilisent ce type) et seule l'implémentation doit contenir les définitions réelles (après avoir inclus d'abord l'en-tête public).


Remarque : depuis la rédaction de cette réponse, la question a été éditée pour ajouter un exemple de code plus détaillé. Pour le moment, je vais simplement laisser ces notes supplémentaires ici :

En général, vous obtenez de meilleures réponses lorsque vous fournissez de meilleures informations. Puisque je ne pouvais pas voir votre code réel, j'ai fait de mon mieux pour expliquer ce qui se passe, vous laissant appliquer cela à votre code réel.

Dans le code que vous avez maintenant ajouté à la question, il y a une dépendance d'en-tête circulaire. Celles-ci devraient être évitées ; il est presque impossible de les gérer correctement. La dépendance circulaire signifie que vous ne contrôlez pas l'ordre d'inclusion, donc une déclaration dans un en-tête pourrait ne pas venir avant l'utilisation dans un autre en-tête. Vous n'avez plus de déclaration avancée, car, en fonction de l'ordre d'inclusion, cela pourrait être une déclaration retardée.

Pour résoudre la dépendance circulaire, isolez les composants partagés et placez-les dans un nouveau fichier d'en-tête. Ici, les déclarations avancées de structs (en utilisant, par exemple, des typedefs) sont très utiles car elles ne dépendent pas de quoi que ce soit utilisé dans la définition de la struct. Un en-tête partagé pourrait ne contenir que des typedefs, ou il pourrait également inclure des prototypes ne nécessitant pas de dépendances supplémentaires.

Évitez également de mettre de longues listes d'inclusions de bibliothèques dans vos fichiers d'en-tête ; incluez uniquement les en-têtes nécessaires pour définir les types réellement utilisés dans l'en-tête.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X