Avertissement étrange du compilateur C: avertissement: ‘struct’ déclaré dans la liste de parameters

Je viens de trouver une bizarrerie dans C que je trouve vraiment déroutant. En C, il est possible d’utiliser un pointeur sur une structure avant qu’elle ne soit déclarée. C’est une fonctionnalité très utile qui a du sens car la déclaration n’est pas pertinente lorsque vous traitez avec un pointeur dessus. Je viens de trouver un cas où ce n’est (étonnamment) pas vrai, cependant, et je ne peux pas vraiment expliquer pourquoi. Pour moi, cela ressemble à une erreur dans la conception du langage.

Prenez ce code:

#include  #include  typedef void (*a)(struct lol* etc); void a2(struct lol* etc) { } int main(void) { return 0; } 

Donne:

 foo.c:6:26: warning: 'struct lol' declared inside parameter list [enabled by default] foo.c:6:26: warning: its scope is only this definition or declaration, which is probably not what you want [enabled by default] foo.c:8:16: warning: 'struct lol' declared inside parameter list [enabled by default] 

Pour supprimer ce problème, nous pouvons simplement faire ceci:

 #include  #include  struct lol* wut; typedef void (*a)(struct lol* etc); void a2(struct lol* etc) { } int main(void) { return 0; } 

Le problème inexplicable est maintenant parti pour une raison inexplicable. Pourquoi?

Notez que cette question concerne le comportement du langage C (ou le comportement possible du compilateur gcc et clang) et non l’exemple spécifique que j’ai collé.

MODIFIER:

Je n’accepterai pas “l’ordre de déclaration est important” comme réponse, à moins que vous expliquiez également pourquoi C mettrait en garde sur l’utilisation d’un pointeur struct pour la première fois dans une liste d’arguments de fonctions, mais l’autorise dans tout autre contexte. Pourquoi cela pourrait-il être un problème?

Pour comprendre pourquoi le compilateur se plaint, vous devez connaître deux choses sur les “structures” de C:

  • ils sont créés (en tant que type déclaré, mais pas encore défini) dès que vous les nommez, de sorte que la toute première occurrence de struct lol crée une déclaration
  • ils obéissent aux mêmes règles de “scope de déclaration” que les variables ordinaires

( struct lol { déclare et commence ensuite à définir la structure, c’est struct lol; ou struct lol * ou quelque chose d’autre qui n’a pas l’accolade ouverte qui s’arrête après l’étape “declare”.)

Un type de structure déclaré mais non encore défini est une instance de ce que C appelle un “type incomplet”. Vous êtes autorisé à utiliser des pointeurs sur des types incomplets, tant que vous n’essayez pas de suivre le pointeur:

 struct lol *global_p; void f(void) { use0(global_p); /* this is OK */ use1(*global_p); /* this is an error */ use2(global_p->field); /* and so is this */ } 

Vous devez compléter le type pour “suivre le pointeur”, autrement dit.

Dans tous les cas, cependant, considérons les déclarations de fonction avec les parameters int ordinaires:

 int imin2(int a, int b); /* returns a or b, whichever is smaller */ int isum2(int a, int b); /* returns a + b */ 

Les variables nommées a et b ici sont déclarées entre parenthèses, mais ces déclarations doivent être gérées de manière à ce que la déclaration de fonction suivante ne se plaint pas de leur nouvelle déclaration.

La même chose se produit avec les noms de balises struct :

 void gronk(struct sttag *p); 

Le struct sttag déclare une structure, puis la déclaration est balayée, tout comme celles de a et b . Mais cela crée un gros problème: la balise a disparu et vous ne pouvez plus jamais nommer le type de structure! Si vous écrivez:

 struct sttag { int field1; char *field2; }; 

qui définit un nouveau et différent struct sttag , juste comme:

 void somefunc(int x) { int y; ... } int x, y; 

définit un x et y nouveaux et différents au niveau du fichier, différents de ceux de somefunc .

Heureusement, si vous déclarez (ou même définissez) la structure avant d’écrire la déclaration de fonction, la déclaration de niveau prototype “renvoie en arrière” à la déclaration de scope extérieure:

 struct sttag; void gronk(struct sttag *p); 

À présent, les deux struct sttag sont “la même” struct sttag , donc lorsque vous terminez la struct sttag ultérieurement, vous complétez celle du prototype de gronk également.


Concernant l’édition des questions: il aurait certainement été possible de définir différemment l’action des balises struct, union et enum, en les faisant “claquer” des prototypes dans leur champ d’application. Cela ferait disparaître le problème. Mais ce n’était pas défini de cette façon. Comme c’est le comité ANSI C89 qui a inventé (ou volé, en réalité, des prototypes à l’époque C ++), vous pouvez le leur reprocher. 🙂

En effet, dans le premier exemple, la structure est précédemment non définie et le compilateur essaie de traiter cette première référence à cette structure comme une définition.

En général, C est une langue où l’ordre de vos déclarations est important. Tout ce que vous utilisez doit être correctement déclaré à l’avance, de manière à ce que le compilateur puisse raisonner à ce sujet lorsqu’il est référencé dans un autre contexte.

Ce n’est pas un bug ou une erreur dans la conception du langage. Je pense plutôt que c’est un choix qui a été fait pour simplifier les implémentations des premiers compilateurs C. Les déclarations anticipées permettent au compilateur de traduire le code source en série en une seule passe (à condition que certaines informations, telles que la taille et les décalages, soient connues). Si ce n’était pas le cas, le compilateur aurait la possibilité de revenir en arrière dans le programme chaque fois qu’il rencontre un identifiant non reconnu, ce qui nécessite que sa boucle d’émission de code soit beaucoup plus complexe.

Le compilateur vous avertit d’une déclaration forward de struct lol . C vous permet de faire ceci:

 struct lol; /* forward declaration, the size and members of struct lol are unknown */ 

Ceci est le plus utilisé lors de la définition de structures auto-référencées, mais également lors de la définition de structures privées qui ne sont jamais définies dans l’en-tête. En raison de ce dernier cas d’utilisation, il est autorisé à déclarer des fonctions qui reçoivent ou renvoient des pointeurs vers des structures incomplètes:

 void foo(struct lol *x); 

Cependant, le fait d’utiliser une structure non déclarée dans une déclaration de fonction, comme vous l’avez fait, sera interprété comme une déclaration locale incomplète de struct lol dont la scope est contrainte à la fonction. Cette interprétation est prescrite par le standard C, mais elle n’est pas utile (il n’existe aucun moyen de construire la struct lol à transmettre à la fonction) et n’est certainement pas ce que le programmeur avait prévu, avertit donc le compilateur.