Typedefs internes en C ++ - bon style ou mauvais style?

179

Quelque chose que je me suis souvent retrouvé à faire ces derniers temps est de déclarer des typedefs pertinents pour une classe particulière à l'intérieur de cette classe, c'est-à-dire

class Lorem
{
    typedef boost::shared_ptr<Lorem> ptr;
    typedef std::vector<Lorem::ptr>  vector;

//
// ...
//
};

Ces types sont ensuite utilisés ailleurs dans le code:

Lorem::vector lorems;
Lorem::ptr    lorem( new Lorem() );

lorems.push_back( lorem );

Raisons pour lesquelles je l'aime:

  • Il réduit le bruit introduit par les modèles de classe, std::vector<Lorem>devient Lorem::vector, etc.
  • Il sert de déclaration d'intention - dans l'exemple ci-dessus, la classe Lorem est destinée à être comptée par référence boost::shared_ptret stockée dans un vecteur.
  • Cela permet à l'implémentation de changer - c'est-à-dire si Lorem devait être changé pour être compté de manière intrusive (via boost::intrusive_ptr) à un stade ultérieur, cela aurait un impact minimal sur le code.
  • Je pense qu'il a l'air plus joli et est sans doute plus facile à lire.

Raisons pour lesquelles je n'aime pas ça:

  • Il y a parfois des problèmes avec les dépendances - si vous voulez incorporer, par exemple, une Lorem::vectordans une autre classe mais que vous avez seulement besoin (ou voulez) de transmettre la déclaration de Lorem (par opposition à l'introduction d'une dépendance sur son fichier d'en-tête), vous finissez par devoir utiliser le types explicites (par exemple boost::shared_ptr<Lorem>plutôt que Lorem::ptr), ce qui est un peu incohérent.
  • Ce n'est peut-être pas très courant et donc plus difficile à comprendre?

J'essaie d'être objectif avec mon style de codage, donc ce serait bien d'avoir d'autres opinions à ce sujet afin que je puisse disséquer un peu ma pensée.

Will Baker
la source

Réponses:

153

Je pense que c'est un excellent style et je l'utilise moi-même. Il est toujours préférable de limiter autant que possible la portée des noms, et l'utilisation de classes est la meilleure façon de le faire en C ++. Par exemple, la bibliothèque C ++ Standard utilise largement les typedefs dans les classes.


la source
C'est un bon point, je me demande qu'il ait l'air plus «joli» était mon subconscient en soulignant délicatement qu'une portée limitée est une bonne chose. Je me demande cependant si le fait que la STL l'utilise principalement dans les modèles de classe en fait un usage subtilement différent? Est-il plus difficile de justifier dans une classe «concrète»?
Will Baker le
1
Eh bien, la bibliothèque standard est composée de modèles plutôt que de classes, mais je pense que la justification est la même pour les deux.
14

Il sert de déclaration d'intention - dans l'exemple ci-dessus, la classe Lorem est destinée à être comptée par référence via boost :: shared_ptr et stockée dans un vecteur.

C'est exactement ce qu'il ne fait pas .

Si je vois 'Foo :: Ptr' dans le code, je n'ai absolument aucune idée si c'est un shared_ptr ou un Foo * (STL a des typedefs :: pointer qui sont T *, rappelez-vous) ou autre. Esp. s'il s'agit d'un pointeur partagé, je ne fournis pas du tout de typedef, mais je garde l'utilisation de shared_ptr explicitement dans le code.

En fait, je n'utilise presque jamais de typedefs en dehors de la métaprogrammation des modèles.

La STL fait ce genre de chose tout le temps

La conception STL avec des concepts définis en termes de fonctions membres et de typedefs imbriqués est un cul-de-sac historique, les bibliothèques de modèles modernes utilisent des fonctions libres et des classes de traits (cf. Boost.Graph), car elles n'excluent pas les types intégrés de modéliser le concept et parce qu'il facilite l'adaptation de types qui n'ont pas été conçus avec les concepts des bibliothèques de modèles donnés à l'esprit.

N'utilisez pas la STL comme une raison de faire les mêmes erreurs.

Marc Mutz - mmutz
la source
Je suis d'accord avec votre première partie, mais votre récente édition est un peu myope. Ces types imbriqués simplifient la définition des classes de traits, car ils fournissent une valeur par défaut raisonnable. Considérez la nouvelle std::allocator_traits<Alloc>classe ... vous n'avez pas à la spécialiser pour chaque allocateur que vous écrivez, car elle emprunte simplement les types directement à partir de Alloc.
Dennis Zickefoose
@Dennis: En C ++, la commodité doit être du côté de / user / d'une bibliothèque, pas du côté de son / author /: l'utilisateur souhaite une interface uniforme pour un trait, et seule une classe de trait peut le donner, pour les raisons exposées ci-dessus). Mais même en tant Allocqu'auteur, il n'est pas vraiment plus difficile de se spécialiser std::allocator_traits<>pour son nouveau type que d'ajouter les typedefs nécessaires. J'ai également modifié la réponse, car ma réponse complète ne correspondait pas à un commentaire.
Marc Mutz - mmutz
Mais il est du côté de l'utilisateur. En tant que l' utilisateur d' allocator_traitsessayer de créer un allocateur personnalisé, je n'ai pas déranger les quinze membres de la classe de traits ... tout ce que je dois faire est de dire typedef Blah value_type;et de fournir les fonctions membres appropriées et la valeur par défaut allocator_traitssera comprendre la du repos. De plus, regardez votre exemple de Boost.Graph. Oui, il fait un usage intensif de la classe de traits ... mais l'implémentation par défaut de graph_traits<G>requêtes simples Gpour ses propres typedefs internes.
Dennis Zickefoose
1
Et même la bibliothèque standard 03 utilise des classes de traits le cas échéant ... la philosophie de la bibliothèque n'est pas d'opérer sur des conteneurs de manière générique, mais d'opérer sur des itérateurs. Il fournit donc une iterator_traitsclasse pour que vos algorithmes génériques puissent facilement rechercher les informations appropriées. Ce qui, encore une fois, demande par défaut à l'itérateur ses propres informations. En résumé, les traits et les typedefs internes ne s'excluent guère mutuellement ... ils se soutiennent.
Dennis Zickefoose
1
@Dennis: iterator_traitsest devenu nécessaire car il T*devrait être un modèle de RandomAccessIterator, mais vous ne pouvez pas insérer les typedefs requis T*. Une fois que nous l'avons fait iterator_traits, les typedefs imbriqués sont devenus redondants, et j'aurais aimé qu'ils aient été supprimés sur-le-champ. Pour la même raison (impossibilité d'ajouter des typedefs internes), T[N]ne modélise pas le Sequenceconcept STL , et vous avez besoin de kludges tels que std::array<T,N>. Boost.Range montre comment définir un concept de séquence moderne qui peut être T[N]modélisé, car il ne nécessite pas de typedefs imbriqués, ni de fonctions membres.
Marc Mutz - mmutz
8

Les typdefs sont définitivement du bon style. Et toutes vos «raisons que j'aime» sont bonnes et correctes.

À propos des problèmes que vous rencontrez avec ça. Eh bien, la déclaration prospective n'est pas un Saint Graal. Vous pouvez simplement concevoir votre code pour éviter les dépendances à plusieurs niveaux.

Vous pouvez déplacer typedef en dehors de la classe mais Class :: ptr est tellement plus joli que ClassPtr que je ne le fais pas. C'est comme avec les espaces de noms pour moi - les choses restent connectées dans le cadre.

Parfois j'ai fait

Trait<Loren>::ptr
Trait<Loren>::collection
Trait<Loren>::map

Et cela peut être par défaut pour toutes les classes de domaine et avec une certaine spécialisation pour certaines.

Mykola Golubyev
la source
3

La STL fait ce type de chose tout le temps - les typedefs font partie de l'interface de nombreuses classes de la STL.

reference
iterator
size_type
value_type
etc...

sont tous des typedefs qui font partie de l'interface pour diverses classes de modèles STL.

Michael Burr
la source
C'est vrai, et je soupçonne que c'est là que je l'ai ramassé pour la première fois. Il semble que ce serait un peu plus facile à justifier? Je ne peux pas m'empêcher de voir les typedefs dans un modèle de classe comme étant plus proches de variables, si vous pensez le long de la ligne de «méta-programmation».
Will Baker le
3

Un autre vote pour que ce soit une bonne idée. J'ai commencé à faire cela en écrivant une simulation qui devait être efficace, à la fois dans le temps et dans l'espace. Tous les types de valeur avaient un typedef Ptr qui a commencé comme un pointeur partagé de boost. J'ai ensuite fait un profilage et changé certains d'entre eux en un pointeur intrusif boost sans avoir à changer le code où ces objets étaient utilisés.

Notez que cela ne fonctionne que lorsque vous savez où les classes vont être utilisées et que toutes les utilisations ont les mêmes exigences. Je n'utiliserais pas cela dans le code de la bibliothèque, par exemple, car vous ne pouvez pas savoir lors de l'écriture de la bibliothèque le contexte dans lequel elle sera utilisée.

KeithB
la source
3

Actuellement, je travaille sur du code, qui utilise intensivement ce type de typedefs. Jusqu'à présent, c'est bien.

Mais j'ai remarqué qu'il y a assez souvent des typedefs itératifs, les définitions sont réparties entre plusieurs classes, et on ne sait jamais vraiment de quel type on a affaire. Ma tâche est de résumer la taille de certaines structures de données complexes cachées derrière ces typedefs - je ne peux donc pas me fier aux interfaces existantes. En combinaison avec trois à six niveaux d'espaces de noms imbriqués, cela devient déroutant.

Alors avant de les utiliser, il y a quelques points à considérer

  • Quelqu'un d'autre a-t-il besoin de ces typedefs? La classe est-elle beaucoup utilisée par d'autres classes?
  • Dois-je raccourcir l'utilisation ou masquer la classe? (En cas de masquage, vous pouvez également penser à des interfaces.)
  • D'autres personnes travaillent-elles avec le code? Comment font-ils? Penseront-ils que c'est plus facile ou deviendront-ils confus?
Bodo
la source
1

Je recommande de déplacer ces typedefs en dehors de la classe. De cette façon, vous supprimez la dépendance directe sur les classes de pointeur et de vecteur partagées et vous ne pouvez les inclure que si nécessaire. À moins que vous n'utilisiez ces types dans votre implémentation de classe, je considère qu'ils ne devraient pas être des typedefs internes.

Les raisons pour lesquelles vous l'aimez sont toujours concordantes, car elles sont résolues par l'alias de type via typedef, pas en les déclarant dans votre classe.

Cătălin Pitiș
la source
Cela poluerait l'espace de noms anonyme avec les typedefs, n'est-ce pas?! Le problème avec typedef est qu'il masque le type réel, ce qui peut provoquer des conflits lorsqu'il est inclus dans / par plusieurs modules, qui sont difficiles à trouver / corriger. C'est une bonne pratique de les contenir dans des espaces de noms ou à l'intérieur de classes.
Indy9000
3
Les conflits de noms et la pollution des espaces de noms anonymes n'ont pas grand-chose à voir avec le maintien d'un nom de type à l'intérieur ou à l'extérieur d'une classe. Vous pouvez avoir un conflit de nom avec votre classe, pas avec vos typedefs. Donc, pour éviter la polution des noms, utilisez des espaces de noms. Déclarez votre classe et les typedefs associés dans un espace de noms.
Cătălin Pitiș
Un autre argument pour placer le typedef dans une classe est l'utilisation de fonctions modélisées. Lorsque, par exemple, une fonction reçoit un type de conteneur inconnu (vecteur ou liste) contenant un type de chaîne inconnu (chaîne ou votre propre variante conforme à la chaîne). la seule façon de déterminer le type de la charge utile du conteneur est d'utiliser le typedef 'value_type' qui fait partie de la définition de la classe de conteneur.
Marius
1

Lorsque le typedef est utilisé uniquement dans la classe elle-même (c'est-à-dire qu'il est déclaré privé), je pense que c'est une bonne idée. Cependant, pour exactement les raisons que vous donnez, je ne l'utiliserais pas si le typedef doit être connu en dehors de la classe. Dans ce cas, je recommande de les déplacer en dehors de la classe.

Stefan Rådström
la source