web-dev-qa-db-fra.com

Meilleures pratiques pour les fichiers d'en-tête pour les typedefs

J'utilise largement shared_ptr et STL dans un projet, ce qui conduit à des types trop longs et sujets aux erreurs comme shared_ptr< vector< shared_ptr<const Foo> > > (Je suis un programmeur ObjC de préférence, où les noms longs sont la norme, et c'est encore beaucoup trop.) Il serait beaucoup plus clair, je crois, d'appeler systématiquement ceci FooListPtr et de documenter le convention de dénomination selon laquelle "Ptr" signifie shared_ptr et "List" signifie vecteur de shared_ptr.

C'est facile à taper, mais cela provoque des maux de tête avec les en-têtes. Il me semble avoir plusieurs options où définir FooListPtr:

  • Foo.h. Cela entrelace tous les en-têtes et crée de graves problèmes de construction, c'est donc un non-démarreur.
  • FooFwd.h ("en-tête avant"). C'est ce que Effective C++ suggère, basé sur iosfwd.h. C'est très cohérent, mais la surcharge de maintenir deux fois le nombre d'en-têtes semble au mieux ennuyeuse.
  • Common.h (tous les rassembler dans un seul fichier). Cela tue la réutilisabilité en imbriquant de nombreux types non liés. Désormais, vous ne pouvez plus simplement prendre un objet et le déplacer vers un autre projet. C'est un non-starter.
  • Une sorte de magie de #define de fantaisie qui typedef si elle n'a pas déjà été typedefed. J'ai une aversion constante pour le préprocesseur parce que je pense qu'il est difficile pour les nouvelles personnes de grogner le code, mais peut-être ...
  • Utilisez une sous-classe vectorielle plutôt qu'un typedef. Cela semble dangereux ...

Y a-t-il des meilleures pratiques ici? Comment se révèlent-ils dans le vrai code, lorsque la réutilisabilité, la lisibilité et la cohérence sont primordiales?

J'ai marqué ce wiki communautaire si d'autres veulent ajouter des options supplémentaires pour la discussion.

51
Rob Napier

Je programme sur un projet qui semble utiliser le common.h méthode. Cela fonctionne très bien pour ce projet.

Il existe un fichier appelé ForwardsDecl.h qui se trouve dans l'en-tête précompilé et déclare simplement toutes les classes importantes et les typedefs nécessaires. Dans ce cas unique_ptr est utilisé à la place de shared_ptr, mais l'utilisation doit être similaire. Cela ressemble à ceci:

// Forward declarations
class ObjectA;
class ObjectB;
class ObjectC;

// List typedefs
typedef std::vector<std::unique_ptr<ObjectA>> ObjectAList;
typedef std::vector<std::unique_ptr<ObjectB>> ObjectBList;
typedef std::vector<std::unique_ptr<ObjectC>> ObjectCList;

Ce code est accepté par Visual C++ 2010 même si les classes sont uniquement déclarées en aval (les définitions de classe complètes ne sont pas nécessaires, il n'est donc pas nécessaire d'inclure le fichier d'en-tête de chaque classe). Je ne sais pas si c'est standard et d'autres compilateurs auront besoin de la définition de classe complète, mais c'est utile que ce ne soit pas le cas: une autre classe (ObjectD) peut avoir un ObjectAList en tant que membre, sans avoir besoin d'inclure ObjectA.h - cela peut vraiment aider à réduire les dépendances du fichier d'en-tête!

La maintenance n'est pas particulièrement un problème, car les déclarations de transfert ne doivent être écrites qu'une seule fois, et toutes les modifications ultérieures doivent uniquement se produire dans la déclaration complète dans le fichier d'en-tête de la classe (et cela entraînera moins de fichiers source à recompiler en raison de la réduction dépendances).

Enfin, il semble que cela puisse être partagé entre des projets (je ne l'ai pas essayé moi-même) car même si un projet ne déclare pas réellement un ObjectA, cela n'a pas d'importance car il n'a été déclaré qu'en avant et si vous ne l'utilisez pas, le compilateur s'en fiche. Par conséquent, le fichier peut contenir les noms des classes dans tous les projets dans lesquels il est utilisé, et peu importe si certaines sont manquantes pour un projet particulier. Tout ce qui est requis est l'en-tête de déclaration complet nécessaire (par exemple ObjectA.h) est inclus dans tous les fichiers source (.cpp) qui les utilisent réellement eux.

13
AshleysBrain

J'irais avec une approche combinée d'en-têtes avant et une sorte de common.h en-tête qui est spécifique à votre projet et qui inclut uniquement tous les en-têtes de déclaration avant et tout autre élément courant et léger.

Vous vous plaignez des frais généraux liés au maintien du double du nombre d'en-têtes, mais je ne pense pas que cela devrait être trop problématique: les en-têtes avancés n'ont généralement besoin de connaître qu'un nombre très limité de types (un?), Et parfois même pas le type complet.

Vous pouvez même essayer de générer automatiquement les en-têtes à l'aide d'un script (cela se fait par exemple dans SeqAn ) s'il y a vraiment qui de nombreux en-têtes.

6
Konrad Rudolph

+1 pour documenter les conventions typedef.

  • Foo.h - pouvez-vous détailler les problèmes que vous avez avec cela?
  • FooFwd.h - Je ne les utiliserais pas en général, uniquement sur des "hotspots évidents". (Oui, les "hotspots" sont difficiles à déterminer). Cela ne change pas les règles IMO car lorsque vous introduisez un en-tête fwd, les typedefs associés de foo.h s'y déplacent.
  • Common.h - cool pour les petits projets, mais ne s'adapte pas, je suis d'accord.
  • Une sorte de fantaisie #define ... S'IL VOUS PLAÎT NON! ...
  • Utiliser une sous-classe vectorielle - ne fait pas mieux. Vous pouvez cependant utiliser le confinement.

Voici donc les suggestions préliminaires (révisées de cette autre question ..)

  1. En-têtes de type standard <boost/shared_ptr.hpp>, <vector> etc. peut entrer dans un en-tête précompilé/fichier inclus partagé pour le projet. Ce n'est pas mauvais. (Personnellement, je les inclue toujours là où c'est nécessaire, mais cela fonctionne en plus de les mettre dans le PCH.)

  2. Si le conteneur est un détail d'implémentation, les typedefs vont là où le conteneur est déclaré (par exemple, les membres de classe privés si le conteneur est un membre de classe privé)

  3. Les types associés (comme FooListPtr) vont à l'endroit où Foo est déclaré, si le type associé est la principale utilisation du type. C'est presque toujours vrai pour certains types - par exemple shared_ptr.

  4. Si Foo obtient un en-tête de déclaration directe distinct et que le type associé est d'accord avec cela, il se déplace également vers FooFwd.h.

  5. Si le type n'est associé qu'à une interface particulière (par exemple un paramètre pour une méthode publique), il y va.

  6. Si le type est partagé (et ne répond à aucun des critères précédents), il obtient son propre en-tête. Notez que cela signifie également extraire toutes les dépendances.

Cela semble "évident" pour moi, mais je suis d'accord que ce n'est pas bon en tant que norme de codage.

4
peterchen

J'utilise largement shared_ptr et STL dans un projet, ce qui conduit à des types trop longs et sujets aux erreurs comme shared_ptr <vector <shared_ptr>> (je suis un programmeur ObjC de préférence, où les noms longs sont la norme, et c'est encore beaucoup trop.) Il serait beaucoup plus clair, je crois, d'appeler systématiquement ce FooListPtr et de documenter la convention de dénomination que "Ptr" signifie shared_ptr et "List" signifie vecteur de shared_ptr.

pour commencer, je recommande d'utiliser de bonnes structures de conception pour la portée (par exemple, les espaces de noms) ainsi que des noms descriptifs et non abrégés pour les typedefs. FooListPtr est terriblement court, imo. personne ne veut deviner ce que signifie une abréviation (ou être surpris de constater que Foo est const, partagé, etc.), et personne ne veut modifier leur code simplement en raison de collisions de portée.

il peut également être utile de choisir un préfixe pour les typedefs dans vos bibliothèques (ainsi que d'autres catégories courantes).

c'est aussi une mauvaise idée de faire glisser les types hors de leur portée déclarée:

namespace MON {
namespace Diddy {
class Foo;
} /* << Diddy */

/*...*/
typedef Diddy::Foo Diddy_Foo;

} /* << MON */

il y a des exceptions à cela:

  • un type privé entièrement ecapsualted
  • un type contenu dans une nouvelle étendue

tant que nous y sommes, using dans les étendues d'espace de noms et les alias d'espaces de noms doit être évité - qualifiez l'étendue si vous voulez minimiser la maintenance future.

C'est facile à taper, mais cela provoque des maux de tête avec les en-têtes. Il me semble avoir plusieurs options où définir FooListPtr:

Foo.h. Cela entrelace tous les en-têtes et crée de graves problèmes de construction, c'est donc un non-démarreur.

il peut s'agir d'une option pour les déclarations qui dépendent vraiment d'autres déclarations. ce qui implique que vous devez diviser les packages, ou qu'il existe une interface commune et localisée pour les sous-systèmes.

FooFwd.h ("en-tête avant"). C'est ce que suggère Effective C++, basé sur iosfwd.h. C'est très cohérent, mais la surcharge de maintenir deux fois le nombre d'en-têtes semble au mieux ennuyeuse.

ne vous inquiétez pas du maintien de cela, vraiment. c'est une bonne pratique. le compilateur utilise les déclarations avancées et les typedefs avec très peu d'effort. ce n'est pas gênant car cela permet de réduire vos dépendances et permet de s'assurer qu'elles sont toutes correctes et visibles. il n'y a vraiment rien de plus à maintenir puisque les autres fichiers se réfèrent à l'en-tête "types de packages".

Common.h (tous les rassembler dans un seul fichier). Cela tue la réutilisabilité en imbriquant de nombreux types non liés. Désormais, vous ne pouvez plus simplement prendre un objet et le déplacer vers un autre projet. C'est un non-starter.

les dépendances et inclusions basées sur les packages sont excellentes (idéal, vraiment) - ne l'excluez pas. vous devrez évidemment créer des interfaces de package (ou bibliothèques) bien conçues et bien structurées, et représentant des classes de composants apparentées. vous faites un problème inutile de réutilisation d'objet/composant. minimisez les données statiques d'une bibliothèque et laissez les phases de liaison et de suppression faire leur travail. encore une fois, gardez vos packages petits et réutilisables et ce ne sera pas un problème (en supposant que vos bibliothèques/packages sont bien conçus).

Une sorte de magie de #define de fantaisie qui typedef si elle n'a pas déjà été typedefed. J'ai une aversion constante pour le préprocesseur parce que je pense qu'il est difficile pour les nouvelles personnes de grogner le code, mais peut-être ...

en fait, vous pouvez déclarer un typedef dans la même portée plusieurs fois (par exemple, dans deux en-têtes distincts) - ce n'est pas une erreur.

déclarer une typedef dans la même portée avec différents types sous-jacents is une erreur. évidemment. vous devez éviter cela, et heureusement, le compilateur applique cela.

pour éviter cela, créez un "build de traduction" qui inclut le monde - le compilateur signalera les déclarations de types typedeffed qui ne correspondent pas.

essayer de se faufiler avec des typedefs et/ou des forwards (qui sont suffisamment proches pour être libérés lors de la compilation) ne vaut pas la peine. Parfois, vous aurez besoin d'un tas de support conditionnel pour les déclarations avancées - une fois que cela est défini, c'est facile (les bibliothèques stl en sont un bon exemple - dans le cas où vous déclarez également en avant template<typename,typename>class vector;).

il est préférable d'avoir juste toutes ces déclarations visibles pour détecter immédiatement toute erreur, et vous pouvez éviter le préprocesseur dans ce cas en bonus.

Utilisez une sous-classe vectorielle plutôt qu'un typedef. Cela semble dangereux ...

une sous-classe de std::vector est souvent signalé comme une "erreur du débutant". ce conteneur n'était pas destiné à être sous-classé. ne recourez pas à de mauvaises pratiques simplement pour réduire vos temps de compilation/dépendances. si la dépendance est vraiment importante, vous devriez probablement utiliser PIMPL, de toute façon:

// <package>.types.hpp
namespace MON {
class FooListPtr;
}

// FooListPtr.hpp
namespace MON {
class FooListPtr {
    /* ... */
private:
    shared_ptr< vector< shared_ptr<const Foo> > > d_data;
};
}

Y a-t-il des meilleures pratiques ici? Comment se révèlent-ils dans le vrai code, lorsque la réutilisabilité, la lisibilité et la cohérence sont primordiales?

en fin de compte, j'ai trouvé une petite approche concise basée sur les packages, la meilleure pour la réutilisation, pour réduire les temps de compilation et minimiser la dépendance.

2
justin

Malheureusement, avec les typedefs, vous devez choisir entre des options non idéales pour vos fichiers d'en-tête. Il existe des cas spéciaux où l'option 1 (directement dans l'en-tête de la classe) fonctionne bien, mais il semble que cela ne fonctionnera pas pour vous. Il existe également des cas où la dernière option fonctionne bien, mais c'est généralement lorsque vous utilisez la sous-classe pour remplacer un modèle impliquant une classe par un seul membre de type std :: vector. Pour votre situation, j'utiliserais la solution d'en-tête de déclaration directe. Il y a une saisie et des frais supplémentaires, mais ce ne serait pas le C++ sinon, non? Il garde les choses séparées, propres et rapides.

1
Michael Daum