44 votes

Typedefs répétés - invalide en C mais valide en C++ ?

J'aimerais avoir une référence standard pour savoir pourquoi le code suivant déclenche un avertissement de conformité en C (testé avec gcc -pedantic ; "redéfinition typedef"), mais elle est parfaite en C++ ( g++ -pedantic ) :

typedef struct Foo Foo;
typedef struct Foo Foo;

int main() { return 0; }

Pourquoi je ne peux pas définir un typedef à plusieurs reprises en C ?

(Ceci a des implications pratiques pour la structuration de l'en-tête d'une Projet C .)

39voto

Alok Save Points 115848

Pourquoi cela compile-t-il en C++ ?

Parce que la norme C++ le dit explicitement.

Référence :

Norme C++03 7.1.3 spécificateur typedef

§7.1.3.2 :

Dans une portée non-classe donnée, un spécificateur typedef peut être utilisé pour redéfinir le nom de tout type déclaré dans cette portée afin de faire référence au type auquel il fait déjà référence.

[Exemple :
typedef struct s { /* ... */ } s ;
typedef int I ;
typedef int I ;
typedef I I ;
-fin de l'exemple]

Pourquoi cela ne compile-t-il pas en C ?

typedef Les noms n'ont pas de lien et la norme C99 interdit aux identifiants sans spécification de lien d'avoir plus d'une déclaration avec la même portée et dans le même espace de nom.

Référence :

Norme C99 : §6.2.2 Liaisons d'identifiers

§6.2.2/6 États :

Les identifiers suivants n'ont pas de lien : un identifier déclaré comme étant autre que un objet ou une fonction ; un identifier déclaré comme étant un paramètre de fonction ; une portée de bloc identifier pour un objet déclaré sans la classe de stockage specifierextern.

Plus d'informations sur §6.7/3 États :

Si un identifiant n'a pas de lien, il ne doit pas y avoir plus d'une déclaration de l'identifiant (dans un déclarateur ou un spécificateur de type) avec la même portée et dans le même espace de noms. sauf pour les balises spécifiées au point 6.7.2.3.

21voto

Jonathan Leffler Points 299946

La norme C est désormais ISO/IEC 9989:2011

La norme C 2011 a été publiée le lundi 2011-12-19 par l'ISO (ou, plus précisément, la notification de sa publication a été ajoutée au site web du comité le 19 ; la norme peut avoir été publiée depuis 2011-12-08). Voir l'annonce sur le site WG14 site web. Malheureusement, le PDF à partir d'ISO coûte 338 CHF, et de ANSI 387 USD .

  • Vous pouvez obtenir le PDF de la norme INCITS/ISO/IEC 9899:2012 (C2011) à l'adresse suivante ANSI pour 30 USD.
  • Vous pouvez obtenir le PDF de la norme INCITS/ISO/IEC 14882:2012 (C++2011) à l'adresse suivante ANSI pour 30 USD.

Réponse principale

La question est la suivante : "Les typographies répétées sont-elles autorisées en C ? La réponse est "Non - pas dans les normes ISO/IEC 9899:1999 ou 9899:1990". La raison est probablement historique : les compilateurs C originaux ne le permettaient pas, et les normalisateurs originaux (qui étaient mandatés pour normaliser ce qui était déjà disponible dans les compilateurs C) ont normalisé ce comportement.

Voir le réponse par Als pour les cas où la norme C99 proscrit les typedefs répétés. La norme C11 a modifié la règle du §6.7 ¶3 comme suit :

3 Si un identificateur n'a pas de lien, il ne doit pas y avoir plus d'une déclaration de l'identificateur (dans un déclarateur ou un spécificateur de type) avec la même portée et dans le même espace de nom, sauf que que :

  • un nom de typedef peut être redéfini pour désigner le même type que celui qu'il désigne actuellement, à condition que ce type ne soit pas un type variablement modifié ;
  • peuvent être redéclarées comme indiqué au point 6.7.2.3.

Il y a donc maintenant un mandat explicite pour un typedef répété dans C11. Il faut attendre la disponibilité de compilateurs C conformes à la norme C11.


Pour ceux qui utilisent encore C99 ou une version antérieure, la question de suivi est alors, vraisemblablement, "Alors comment éviter de rencontrer des problèmes avec les typedefs répétés ?".

Si vous suivez la règle selon laquelle il n'y a qu'un seul en-tête qui définit chaque type qui est nécessaire dans plus d'un fichier source (mais il peut y avoir de nombreux en-têtes définissant ces types ; chaque type distinct ne se trouve cependant que dans un seul en-tête), et si cet en-tête est utilisé à chaque fois que ce type est nécessaire, alors vous ne rencontrez pas de conflit.

Vous pouvez également utiliser des déclarations de structure incomplètes si vous n'avez besoin que de pointeurs vers les types et n'avez pas besoin d'allouer la structure réelle ou d'accéder à ses membres (types opaques). Encore une fois, définissez des règles concernant l'en-tête qui déclare le type incomplet, et utilisez cet en-tête partout où le type est nécessaire.

Voir aussi Que sont les variables externes en C ; elle parle de variables, mais les types peuvent être traités de manière assez similaire.


Question des commentaires

J'ai vraiment besoin des "déclarations de structure incomplètes", en raison de complications distinctes du préprocesseur qui interdisent certaines inclusions. Donc vous dites que je ne dois pas typer ces déclarations avant si elles sont typées à nouveau par l'en-tête complet ?

Plus ou moins. Je n'ai pas vraiment eu à m'occuper de cela (bien que certaines parties des systèmes au travail soient très proches de ce problème), donc c'est un peu provisoire, mais je pense que cela devrait fonctionner.

En général, un en-tête décrit les services externes fournis par une "bibliothèque" (un ou plusieurs fichiers source) de manière suffisamment détaillée pour que les utilisateurs de la bibliothèque puissent compiler avec elle. En particulier dans le cas où il y a plusieurs fichiers source, il peut également y avoir un en-tête interne qui définit, par exemple, les types complets.

Tous les en-têtes sont (a) autonomes et (b) idempotents. Cela signifie que vous pouvez (a) inclure l'en-tête et tous les autres en-têtes requis sont automatiquement inclus, et (b) vous pouvez inclure l'en-tête plusieurs fois sans encourir la colère du compilateur. Ce dernier point est généralement obtenu grâce à des gardes d'en-tête, bien que certains préfèrent l'option #pragma once - mais ce n'est pas portable.

Ainsi, vous pouvez avoir un en-tête public comme celui-ci :

public.h

#ifndef PUBLIC_H_INCLUDED
#define PUBLIC_H_INCLUDED

#include <stddef.h>    // size_t

typedef struct mine mine;
typedef struct that that;

extern size_t polymath(const mine *x, const that *y, int z);

#endif /* PUBLIC_H_INCLUDED */

Jusqu'ici, pas de grande controverse (bien que l'on puisse légitimement soupçonner que l'interface fournie par cette bibliothèque est très incomplète).

private.h

#ifndef PRIVATE_H_INCLUDED
#define PRIVATE_H_INCLUDED

#include "public.h"  // Get forward definitions for mine and that types

struct mine { ... };
struct that { ... };

extern mine *m_constructor(int i);
...

#endif /* PRIVATE_H_INCLUDED */

Encore une fois, ce n'est pas très controversé. Le site public.h L'en-tête doit être listé en premier ; cela permet de vérifier automatiquement l'auto-contrôle.

Code du consommateur

Tout code qui nécessite le polymath() services écrit :

#include "public.h"

Ce sont toutes les informations nécessaires pour utiliser le service.

Code du fournisseur

Tout code de la bibliothèque qui définit le polymath() services écrit :

#include "private.h"

Par la suite, tout fonctionne normalement.

Autre code de fournisseur

S'il y a une autre bibliothèque (appelez-la multimath() ) qui utilise le polymath() services, alors ce code doit inclure public.h comme n'importe quel autre consommateur. Si le polymath() font partie de l'interface externe de multimath() alors le multimath.h L'en-tête public comprendra public.h (désolé, j'ai changé de terminologie vers la fin, ici). Si le multimath() services dissimulent complètement le polymath() services, alors le multimath.h L'en-tête ne comprendra pas public.h mais le multimath() ou les fichiers sources individuels qui ont besoin de l'en-tête privé peuvent le faire. polymath() peuvent l'inclure si nécessaire.

Tant que vous suivez religieusement la discipline consistant à inclure l'en-tête correct partout, vous ne rencontrerez pas de problème de double définition.

Si vous constatez par la suite qu'un de vos en-têtes contient deux groupes de définitions, l'un qui peut être utilisé sans conflit et l'autre qui peut parfois (ou toujours) entrer en conflit avec un nouvel en-tête (et les services qui y sont déclarés), vous devez alors diviser l'en-tête original en deux sous-en-têtes. Chaque sous-en-tête suit individuellement les règles élaborées ici. L'en-tête original devient trivial - une garde d'en-tête et des lignes pour inclure les deux fichiers individuels. Tout le code de travail existant reste inchangé - bien que les dépendances changent (fichiers supplémentaires à dépendre). Le nouveau code peut maintenant inclure le sous-en-tête acceptable pertinent tout en utilisant le nouvel en-tête qui entre en conflit avec l'en-tête original.

Bien sûr, vous pouvez avoir deux en-têtes qui sont tout simplement inconciliables. Pour un exemple artificiel, s'il existe un en-tête (mal conçu) qui déclare une version différente de l'en-tête FILE (à partir de la version dans <stdio.h> ), vous êtes fichu ; le code peut inclure soit l'en-tête mal conçu, soit l'option <stdio.h> mais pas les deux. Dans ce cas, l'en-tête mal conçu devrait être révisé pour utiliser un nouveau nom (peut-être File mais peut-être quelque chose d'autre). Vous pourriez rencontrer ce problème de manière plus réaliste si vous deviez fusionner le code de deux produits en un seul après un rachat d'entreprise, avec certaines structures de données communes, telles que DB_Connection pour une connexion à une base de données. En l'absence de l'option C++ namespace vous vous retrouvez avec un exercice de renommage pour un ou deux lots de code.

8voto

Steve Jessop Points 166970

Vous pouvez le faire en C++ grâce à 7.1.3/3 et /4.

Vous ne pouvez pas le faire en C99 parce qu'il n'y a pas de cas spécial équivalent en 6.7.7, donc la re-déclaration d'un nom de typedef suit les mêmes règles que la re-déclaration de tout autre identifiant. Spécifiquement 6.2.2/6 (les typedefs n'ont pas de lien) et 6.7/3 (les identificateurs sans lien ne peuvent être déclarés qu'une fois avec la même portée).

Souvenez-vous de typedef est un spécificateur de classe de stockage en C99, alors qu'en C++ c'est un spécificateur de déclaration. La différence de grammaire m'amène à suspecter que les auteurs du C++ ont décidé de faire plus d'efforts pour faire des typedefs "un type différent de déclaration", et donc ont pu être disposés à passer plus de temps et de texte sur des règles spéciales pour eux. Au-delà de cela, je ne sais pas quelle était la motivation (ou le manque de motivation) des auteurs de C99.

[Edit : voir la réponse de Johannes pour C1x. Je ne suis pas du tout ce qui se passe, donc je devrais probablement arrêter d'utiliser "C" pour signifier "C99" parce que je ne le remarquerai probablement même pas quand ils ratifieront et publieront. C'est déjà assez mauvais comme ça : "C" devrait signifier "C99", mais en pratique signifie "C99 si vous avez de la chance, mais si vous devez supporter MSVC alors C89"].

(Editer à nouveau : et en effet, il a été publié et est maintenant C11. Woot.)

4voto

Kai Petzke Points 191

Beaucoup de personnes ont répondu en se référant aux normes, mais personne n'a dit POURQUOI les normes diffèrent pour C et C++ ici. Eh bien, je crois que la raison pour laquelle on autorise les typedefs répétés en C++ est que le C++ déclare implicitement les structures et les classes comme des types. Ce qui suit est donc légal en C++ :

struct foo { int a; int b; };
foo f;

En C, il faut écrire :

struct foo { int a; int b; };
typedef struct foo foo;
foo f;

Il y a beaucoup de code C comme ça, qui déclare les structs comme des types. Si un tel code est migré vers C++, les typedefs deviennent dupliqués, parce que le langage C++ ajoute ses propres typedefs implicites. Ainsi, pour éviter aux programmeurs d'avoir à supprimer ces typedefs qui ne sont plus nécessaires, on a autorisé les typedefs dupliqués en C++ dès le début.

Comme d'autres l'ont dit, les gens ont réalisé, avec le temps, que permettre des typologies identiques répétées en C pouvait aussi être utile. Au moins, cela ne devrait pas nuire. C'est pourquoi cette fonctionnalité C++ a été en quelque sorte "rétroportée" dans C11.

3voto

Il n'y a rien dans la spécification C qui dit que pourquoi c'est invalide. La spécification n'est pas le bon endroit pour clarifier cela. Pour info, c'est autorisé en C1x (d'après une réponse que j'ai reçue à l'une de mes dernières questions).

Je suppose que cette fonctionnalité c1x supporte la transformation des macros en typedefs (les premières sont autorisées à être répétées si elles sont identiques).

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