Je suis nouveau dans la programmation en général, j'ai donc décidé de commencer par créer une simple classe vectorielle en C ++. Cependant, j'aimerais avoir de bonnes habitudes dès le départ plutôt que d'essayer de modifier mon flux de travail plus tard.
Je n'ai actuellement que deux fichiers vector3.hpp
et vector3.cpp
. Ce projet commencera lentement à se développer (ce qui en fera beaucoup plus une bibliothèque générale d'algèbre linéaire) au fur et à mesure que je me familiariserai avec tout, donc j'aimerais adopter une disposition de projet "standard" pour me faciliter la vie plus tard. Donc, après avoir regardé autour de moi, j'ai trouvé deux façons d'organiser les fichiers hpp et cpp, la première étant:
project
└── src
├── vector3.hpp
└── vector3.cpp
et le second étant:
project
├── inc
│ └── project
│ └── vector3.hpp
└── src
└── vector3.cpp
lequel recommandez vous et pourquoi?
Deuxièmement, je voudrais utiliser le cadre de test Google C ++ pour tester mon code unitaire car il semble assez facile à utiliser. Proposez-vous de le regrouper avec mon code, par exemple dans un dossier inc/gtest
ou contrib/gtest
? S'il est fourni, suggérez-vous d'utiliser le fuse_gtest_files.py
script pour réduire le nombre de fichiers ou de le laisser tel quel? Si ce n'est pas groupé, comment cette dépendance est-elle gérée?
Lorsqu'il s'agit d'écrire des tests, comment sont-ils généralement organisés? Je pensais avoir un fichier cpp pour chaque classe ( test_vector3.cpp
par exemple) mais tous compilés dans un seul binaire afin qu'ils puissent tous être exécutés ensemble facilement?
Puisque la bibliothèque gtest est généralement construite avec cmake et make, je pensais qu'il serait logique que mon projet soit également construit comme ça? Si j'ai décidé d'utiliser la mise en page de projet suivante:
├── CMakeLists.txt
├── contrib
│ └── gtest
│ ├── gtest-all.cc
│ └── gtest.h
├── docs
│ └── Doxyfile
├── inc
│ └── project
│ └── vector3.cpp
├── src
│ └── vector3.cpp
└── test
└── test_vector3.cpp
À quoi devrait CMakeLists.txt
ressembler le logiciel pour pouvoir soit construire uniquement la bibliothèque, soit la bibliothèque et les tests? J'ai aussi vu pas mal de projets qui ont un build
et un bin
répertoire. La construction se produit-elle dans le répertoire de construction, puis les binaires sont-ils déplacés vers le répertoire bin? Les binaires pour les tests et la bibliothèque vivraient-ils au même endroit? Ou serait-il plus logique de le structurer comme suit:
test
├── bin
├── build
└── src
└── test_vector3.cpp
J'aimerais également utiliser doxygen pour documenter mon code. Est-il possible que cela s'exécute automatiquement avec cmake et make?
Désolé pour tant de questions, mais je n'ai pas trouvé de livre sur C ++ qui réponde de manière satisfaisante à ce type de questions.
la source
Réponses:
Les systèmes de construction C ++ sont un peu un art noir et plus le projet est ancien, plus vous pouvez trouver de choses étranges, il n'est donc pas surprenant que beaucoup de questions se posent. Je vais essayer de parcourir les questions une par une et de mentionner certaines choses générales concernant la construction de bibliothèques C ++.
Séparation des en-têtes et des fichiers cpp dans les répertoires. Ceci n'est essentiel que si vous créez un composant censé être utilisé comme bibliothèque plutôt que comme application réelle. Vos en-têtes sont la base pour les utilisateurs d'interagir avec ce que vous proposez et doivent être installés. Cela signifie qu'ils doivent être dans un sous-répertoire (personne ne veut que beaucoup d'en-têtes se retrouvent au niveau supérieur
/usr/include/
) et que vos en-têtes doivent pouvoir s'inclure dans une telle configuration.fonctionne bien, car les chemins d'inclusion fonctionnent et vous pouvez utiliser un globbing facile pour les cibles d'installation.
Regroupement des dépendances: je pense que cela dépend en grande partie de la capacité du système de construction à localiser et configurer les dépendances et à quel point votre code dépend d'une seule version. Cela dépend également de la capacité de vos utilisateurs et de la facilité d'installation de la dépendance sur leur plate-forme. CMake est livré avec un
find_package
script pour Google Test. Cela rend les choses beaucoup plus faciles. J'irais avec le groupage uniquement lorsque cela est nécessaire et je l'éviterais autrement.Comment construire: évitez les builds in-source. CMake facilite la création de sources et rend la vie beaucoup plus facile.
Je suppose que vous souhaitez également utiliser CTest pour exécuter des tests pour votre système (il est également livré avec un support intégré pour GTest). Une décision importante pour la mise en page des répertoires et l'organisation des tests sera la suivante: vous retrouvez-vous avec des sous-projets? Si tel est le cas, vous avez besoin d'un peu plus de travail lors de la configuration de CMakeLists et devez diviser vos sous-projets en sous-répertoires, chacun avec ses propres
include
src
fichiers et . Peut-être même leurs propres exécutions et sorties doxygen (combiner plusieurs projets doxygen est possible, mais pas facile ni joli).Vous vous retrouverez avec quelque chose comme ceci:
où
Au cas où vous auriez des sous-composants, je suggérerais d'ajouter une autre hiérarchie et d'utiliser l'arborescence ci-dessus pour chaque sous-projet. Ensuite, les choses se compliquent, car vous devez décider si les sous-composants recherchent et configurent leurs dépendances ou si vous le faites au niveau supérieur. Cela devrait être décidé au cas par cas.
Doxygen: Après avoir réussi à passer par la danse de configuration de doxygen, il est trivial d'utiliser CMake
add_custom_command
pour ajouter une cible doc.C'est ainsi que mes projets se terminent et j'ai vu des projets très similaires, mais bien sûr, ce n'est pas un remède.
Addendum À un moment donné, vous voudrez générer un
config.hpp
fichier qui contient une définition de version et peut-être une définition pour un identifiant de contrôle de version (un hachage Git ou un numéro de révision SVN). CMake a des modules pour automatiser la recherche de ces informations et pour générer des fichiers. Vous pouvez utiliser CMakeconfigure_file
pour remplacer des variables dans un fichier modèle par des variables définies dans leCMakeLists.txt
.Si vous construisez des bibliothèques, vous aurez également besoin d'une définition d'exportation pour faire la différence entre les compilateurs, par exemple
__declspec
sur MSVC et lesvisibility
attributs sur GCC / clang.la source
Pour commencer, il existe des noms conventionnels de répertoires que vous ne pouvez pas ignorer, ils sont basés sur la longue tradition du système de fichiers Unix. Ceux-ci sont:
C'est probablement une bonne idée de s'en tenir à cette mise en page de base, au moins au niveau supérieur.
À propos du fractionnement des fichiers d'en-tête et des fichiers source (cpp), les deux schémas sont assez courants. Cependant, j'ai tendance à préférer les garder ensemble, il est simplement plus pratique dans les tâches quotidiennes de rassembler les fichiers. De plus, lorsque tout le code est dans un dossier de niveau supérieur, c'est-à-dire le
trunk/src/
dossier, vous pouvez remarquer que tous les autres dossiers (bin, lib, include, doc, et peut-être un dossier de test) au niveau supérieur, en plus de le répertoire "build" pour une build out-of-source, sont tous les dossiers qui ne contiennent rien de plus que des fichiers qui sont générés dans le processus de build. Et ainsi, seul le dossier src doit être sauvegardé, ou mieux, conservé sous un système / serveur de contrôle de version (comme Git ou SVN).Et quand il s'agit d'installer vos fichiers d'en-tête sur le système de destination (si vous voulez éventuellement distribuer votre bibliothèque), eh bien, CMake a une commande pour installer les fichiers (crée implicitement une cible "install", pour faire "make install") qui vous pouvez utiliser pour mettre tous les en-têtes dans le
/usr/include/
répertoire. J'utilise juste la macro cmake suivante à cet effet:Où se
SRCROOT
trouve une variable cmake que j'ai définie dans le dossier src, etINCLUDEROOT
est la variable cmake que je configure à l'endroit où les en-têtes doivent aller. Bien sûr, il existe de nombreuses autres façons de le faire, et je suis sûr que ma méthode n'est pas la meilleure. Le fait est qu'il n'y a aucune raison de diviser les en-têtes et les sources simplement parce que seuls les en-têtes doivent être installés sur le système cible, car il est très facile, en particulier avec CMake (ou CPack), de choisir et de configurer les en-têtes pour être installés sans avoir à les avoir dans un répertoire séparé. Et c'est ce que j'ai vu dans la plupart des bibliothèques.Ne regroupez pas les dépendances avec votre bibliothèque. C'est généralement une idée assez horrible, et je déteste toujours ça quand je suis coincé à essayer de construire une bibliothèque qui a fait ça. Cela devrait être votre dernier recours et méfiez-vous des pièges. Souvent, les gens regroupent les dépendances avec leur bibliothèque soit parce qu'ils ciblent un environnement de développement terrible (par exemple, Windows), soit parce qu'ils ne supportent qu'une ancienne version (obsolète) de la bibliothèque (dépendance) en question. Le principal piège est que votre dépendance groupée peut entrer en conflit avec des versions déjà installées de la même bibliothèque / application (par exemple, vous avez groupé gtest, mais la personne essayant de construire votre bibliothèque a déjà une version plus récente (ou plus ancienne) de gtest déjà installée, alors les deux pourraient entrer en conflit et donner à cette personne un mal de tête très méchant). Alors, comme je l'ai dit, fais-le à tes risques et périls, et je dirais qu'en dernier recours. Demander aux gens d'installer quelques dépendances avant de pouvoir compiler votre bibliothèque est un bien moindre mal que d'essayer de résoudre les conflits entre vos dépendances groupées et les installations existantes.
Un fichier cpp par classe (ou petit groupe cohésif de classes et de fonctions) est à mon avis plus courant et pratique. Cependant, ne les compilez pas tous dans un seul binaire juste pour "qu'ils puissent tous être exécutés ensemble". C'est une très mauvaise idée. En général, quand il s'agit de codage, vous voulez diviser les choses autant qu'il est raisonnable de le faire. Dans le cas des tests unitaires, vous ne voulez pas qu'un binaire exécute tous les tests, car cela signifie que tout petit changement que vous apportez à quoi que ce soit dans votre bibliothèque est susceptible de provoquer une recompilation presque totale de ce programme de test unitaire , et ce ne sont que des minutes / heures perdues en attente de recompilation. Tenez-vous en à un schéma simple: 1 unité = 1 programme de test unitaire. Ensuite,
Je suggérerais plutôt cette mise en page:
Quelques points à noter ici. Tout d'abord, les sous-répertoires de votre répertoire src doivent refléter les sous-répertoires de votre répertoire d'inclusion, c'est juste pour garder les choses intuitives (aussi, essayez de garder la structure de votre sous-répertoire raisonnablement plate (superficielle), car l'imbrication profonde des dossiers est souvent plus compliqué qu'autre chose). Deuxièmement, le répertoire "include" n'est qu'un répertoire d'installation, son contenu correspond à tous les en-têtes sélectionnés dans le répertoire src.
Troisièmement, le système CMake est destiné à être distribué sur les sous-répertoires source, et non comme un fichier CMakeLists.txt au niveau supérieur. Cela maintient les choses locales, et c'est bien (dans l'esprit de diviser les choses en morceaux indépendants). Si vous ajoutez une nouvelle source, un nouvel en-tête ou un nouveau programme de test, il vous suffit d'éditer un petit et simple fichier CMakeLists.txt dans le sous-répertoire en question, sans rien affecter d'autre. Cela vous permet également de restructurer facilement les répertoires (les CMakeLists sont locales et contenues dans les sous-répertoires déplacés). Les CMakeLists de niveau supérieur doivent contenir la plupart des configurations de niveau supérieur, telles que la configuration des répertoires de destination, des commandes personnalisées (ou macros) et la recherche de packages installés sur le système. Les CMakeLists de niveau inférieur ne doivent contenir que de simples listes d'en-têtes, de sources,
La réponse de base est que CMake vous permet d'exclure spécifiquement certaines cibles de "tous" (ce qui est construit lorsque vous tapez "make"), et vous pouvez également créer des ensembles spécifiques de cibles. Je ne peux pas faire un didacticiel CMake ici, mais il est assez simple de le découvrir par vous-même. Dans ce cas précis, cependant, la solution recommandée est, bien sûr, d'utiliser CTest, qui est juste un ensemble supplémentaire de commandes que vous pouvez utiliser dans les fichiers CMakeLists pour enregistrer un certain nombre de cibles (programmes) qui sont marquées comme unité- des tests. Donc, CMake placera tous les tests dans une catégorie spéciale de builds, et c'est exactement ce que vous avez demandé, donc le problème est résolu.
Avoir un répertoire de construction en dehors de la source (version "out-of-source") est vraiment la seule chose sensée à faire, c'est la norme de facto de nos jours. Donc, définitivement, ayez un répertoire "build" séparé, en dehors du répertoire source, tout comme le recommandent les gens de CMake, et comme le font tous les programmeurs que j'ai rencontrés. Quant au répertoire bin, eh bien, c'est une convention, et c'est probablement une bonne idée de s'y tenir, comme je l'ai dit au début de cet article.
Oui. C'est plus que possible, c'est génial. Selon la fantaisie que vous souhaitez obtenir, il existe plusieurs possibilités. CMake a un module pour Doxygen (c'est-à-dire
find_package(Doxygen)
) qui vous permet d'enregistrer des cibles qui exécuteront Doxygen sur certains fichiers. Si vous voulez faire des choses plus sophistiquées, comme mettre à jour le numéro de version dans le Doxyfile, ou entrer automatiquement une date / des timbres d'auteur pour les fichiers source et ainsi de suite, tout est possible avec un peu de CMake kung-fu. Généralement, cela impliquera que vous gardiez un Doxyfile source (par exemple, le "Doxyfile.in" que j'ai mis dans la disposition du dossier ci-dessus) qui a des jetons à trouver et à remplacer par les commandes d'analyse de CMake. Dans mon fichier CMakeLists de niveau supérieur , vous trouverez un tel morceau de CMake kung-fu qui fait quelques choses fantaisistes avec cmake-doxygen ensemble.la source
main.cpp
devrait allertrunk/bin
?Structurer le projet
Je préférerais généralement ce qui suit:
Cela signifie que vous avez un ensemble très clairement défini de fichiers API pour votre bibliothèque, et la structure signifie que les clients de votre bibliothèque feraient l'affaire
plutôt que le moins explicite
J'aime que la structure de l'arborescence / src corresponde à celle de l'arborescence / include, mais c'est vraiment une préférence personnelle. Cependant, si votre projet se développe pour contenir des sous-répertoires dans / include / project, il serait généralement utile de faire correspondre ceux à l'intérieur de l'arborescence / src.
Pour les tests, je préfère les garder "proches" des fichiers qu'ils testent, et si vous vous retrouvez avec des sous-répertoires dans / src, c'est un paradigme assez facile pour les autres à suivre s'ils veulent trouver le code de test d'un fichier donné.
Essai
Gtest est en effet simple à utiliser et est assez complet en termes de capacités. Il peut être utilisé avec gmock très facilement pour étendre ses capacités, mais mes propres expériences avec gmock ont été moins favorables. Je suis tout à fait prêt à accepter que cela puisse être dû à mes propres défauts, mais les tests gmock ont tendance à être plus difficiles à créer, et beaucoup plus fragiles / difficiles à maintenir. Un gros clou dans le cercueil gmock est qu'il ne joue vraiment pas bien avec des pointeurs intelligents.
C'est une réponse très triviale et subjective à une énorme question (qui n'appartient probablement pas vraiment à SO)
Je préfère utiliser le
ExternalProject_Add
module de CMake . Cela vous évite d'avoir à conserver le code source gtest dans votre référentiel ou à l'installer n'importe où. Il est téléchargé et intégré automatiquement dans votre arborescence de construction.Voir ma réponse traitant des détails ici .
Bon plan.
Bâtiment
Je suis un fan de CMake, mais comme pour vos questions liées aux tests, SO n'est probablement pas le meilleur endroit pour demander des opinions sur une question aussi subjective.
La bibliothèque apparaîtra comme une cible "ProjectLibrary" et la suite de tests comme une cible "ProjectTest". En spécifiant la bibliothèque comme dépendance de l'exe de test, la construction de l'exe de test entraînera automatiquement la reconstruction de la bibliothèque si elle est obsolète.
CMake recommande les builds "hors source", c'est-à-dire que vous créez votre propre répertoire de build en dehors du projet et exécutez CMake à partir de là. Cela évite de «polluer» votre arborescence source avec des fichiers de construction, et est hautement souhaitable si vous utilisez un vcs.
Vous pouvez spécifier que les binaires sont déplacés ou copiés dans un répertoire différent une fois construits, ou qu'ils sont créés par défaut dans un autre répertoire, mais ce n'est généralement pas nécessaire. CMake fournit des moyens complets pour installer votre projet si vous le souhaitez, ou permet aux autres projets CMake de "trouver" facilement les fichiers pertinents de votre projet.
En ce qui concerne la prise en charge par CMake de la recherche et de l'exécution des tests gtest , cela serait largement inapproprié si vous construisez gtest dans le cadre de votre projet. Le
FindGtest
module est vraiment conçu pour être utilisé dans le cas où gtest a été construit séparément en dehors de votre projet.CMake fournit son propre cadre de test (CTest), et idéalement, chaque cas gtest serait ajouté en tant que cas CTest.
Cependant, la
GTEST_ADD_TESTS
macro fournie parFindGtest
pour permettre l'ajout facile de cas gtest en tant que cas ctest individuels manque quelque peu dans la mesure où elle ne fonctionne pas pour les macros de gtest autres queTEST
etTEST_F
. VALUE- ou -type paramétrées tests à l' aideTEST_P
,TYPED_TEST_P
etc. ne sont pas traitées du tout.Le problème n'a pas de solution facile que je connaisse. Le moyen le plus robuste d'obtenir une liste de cas gtest est d'exécuter l'exe de test avec l'indicateur
--gtest_list_tests
. Cependant, cela ne peut être fait qu'une fois que l'exe est construit, donc CMake ne peut pas l'utiliser. Ce qui vous laisse avec deux choix; CMake doit essayer d'analyser le code C ++ pour en déduire les noms des tests (non trivial à l'extrême si vous voulez prendre en compte toutes les macros gtest, les tests commentés, les tests désactivés), ou les cas de test sont ajoutés manuellement au Fichier CMakeLists.txt.Oui - même si je n'ai aucune expérience sur ce front. CMake fournit
FindDoxygen
à cet effet.la source
En plus des autres (excellentes) réponses, je vais décrire une structure que j'utilise pour des projets de relativement grande envergure .
Je ne vais pas aborder la sous-question concernant Doxygen, car je voudrais simplement répéter ce qui est dit dans les autres réponses.
Raisonnement
Pour la modularité et la maintenabilité, le projet est organisé comme un ensemble de petites unités. Pour plus de clarté, appelons-les UnitX, avec X = A, B, C, ... (mais ils peuvent avoir n'importe quel nom général). La structure des répertoires est alors organisée pour refléter ce choix, avec la possibilité de regrouper les unités si nécessaire.
Solution
La structure de base du répertoire est la suivante (le contenu des unités est détaillé plus loin):
project/CMakeLists.txt
pourrait contenir les éléments suivants:et
project/GroupA/CMakeLists.txt
:et
project/GroupB/CMakeLists.txt
:Passons maintenant à la structure des différentes unités (prenons, par exemple, UnitD)
Aux différents composants:
.cpp
) et headers (.h
) dans le même dossier. Cela évite une hiérarchie de répertoires en double, facilite la maintenance. Pour l'installation, ce n'est pas un problème (surtout avec CMake) de simplement filtrer les fichiers d'en-tête.UnitD
est d'autoriser ultérieurement l'inclusion de fichiers avec#include <UnitD/ClassA.h>
. De plus, lors de l'installation de cet appareil, vous pouvez simplement copier la structure des répertoires telle quelle. Notez que vous pouvez également organiser vos fichiers source dans des sous-répertoires.README
fichier pour résumer le sujet de l'unité et spécifier des informations utiles à son sujet.CMakeLists.txt
pourrait simplement contenir:lib/CMakeLists.txt
:Ici, notez qu'il n'est pas nécessaire d'indiquer à CMake que nous voulons les répertoires d'inclusion pour
UnitA
etUnitC
, car cela a déjà été spécifié lors de la configuration de ces unités. De plus,PUBLIC
indiquera à toutes les cibles qui en dépendentUnitD
qu'elles doivent automatiquement inclure laUnitA
dépendance, alors qu'elles neUnitC
seront pas requises (PRIVATE
).test/CMakeLists.txt
(voir ci-dessous si vous souhaitez utiliser GTest pour cela):Utilisation de GoogleTest
Pour Google Test, le plus simple est si sa source est présente quelque part dans votre répertoire source, mais vous n'avez pas à l'ajouter vous-même. J'ai utilisé ce projet pour le télécharger automatiquement, et j'enveloppe son utilisation dans une fonction pour m'assurer qu'il n'est téléchargé qu'une seule fois, même si nous avons plusieurs cibles de test.
Cette fonction CMake est la suivante:
puis, lorsque je veux l'utiliser dans l'une de mes cibles de test, j'ajouterai les lignes suivantes au
CMakeLists.txt
(c'est pour l'exemple ci-dessus,test/CMakeLists.txt
):la source