Série 18 (Niveau 0):
Compilation séparée : cmake
Buts
Jusqu'ici chacun de vos programmes était entièrement rédigé dans un seul fichier. Pour des programmes plus ambitieux et plus importants en taille (ce que l'on appelle des projets), il est important d'opter pour une approche plus modulaire où plusieurs fichiers sont utilisés pour stocker différents composants du projet.Un programme "éclaté" sur plusieurs fichiers est laborieux à compiler à la main. Il existe des alternatives automatisant le processus et cet exercice a pour but de vous montrer comment fonctionne l'un deux :CMake. CMake est ce que l'on appelle un outil de construction de projet (compilation + génération de l'exécutable).
Nous profiterons de cet exercice pour introduire certaines bonnes pratiques concernant l'organisation de vos fichiers dans un projet.
Commencez par créer un répertoire ~/Desktop/myfiles/Programmation/cpp/serie18/exoCmake/. Vous y ferez les manipulations suggérées dans l'exercice.
Un programme sur plusieurs fichiers
Soit le programme suivant, contenu dans un fichier Application.cpp :
#include <iostream> #include <string> using namespace std; // une classe pour représenter une personne class Personne { // quelques méthodes utiles public: // méthode d'initialisation (à remplacer par un // constructeur dès que vus en cours!!) Personne(string nom,string prenom,int age) : nom(nom) , prenom(prenom) , age(age) { // Rien } // méthode affichant les infos d'une personne void afficher() { cout << "Nom: " << this->nom << endl; cout << "Prenom: " << this->prenom << endl; cout << "Age: " << this->age << endl; } //incremente l'age de la personne void anniversaire() { age++; } //déclaration des attributs en privé, comme il se doit private: std::string nom; //le nom std::string prenom; //le prenom int age;//l'age }; //PROGRAMME PRINCIPAL int main (){ Personne stan("Wawrinka","Stan", 33); stan.afficher(); return 0; }
Il contient la définition d'une classe Personne (lignes 6 à 38) utilisée par un programme principal (lignes 40 à 44).
Pour compiler un tel programme, vous êtes habitués à :
- utiliser g++ (ou autres variantes telles que g++-8) suivi éventuellement de certaines options (flags)
- ou à utiliser le bouton build d'un éditeur comme Geany, qui fait exactement la même chose.
En examinant le contenu, on peut se dire que la classe Personne peut-être utile dans bien d'autres contextes que celui du programme principal de Application.cpp !
Il serait donc bien d'en faire un module à part.
Pour créer un module relatif à la classe Personne, on utilisera deux fichiers :
- Personne.hpp (ou .h) qui contient uniquement le prototype de la classe (la coquille de la classe et uniquement les prototypes de méthodes à l'intérieur):
#pragma once // a propos de cette ligne voir la note ci-dessous // N'incluez que ce que le fichier courant utilise #include <string> // using namespace std; // A EVITER DANS UN .hpp/.h class Personne { public: // initialisation // si on n'utilise pas using namespace std; // il faut écrire std::string Personne(std::string nom, std::string prenom, int age); //affiche les infos d'une personne void afficher(); //incremente l'age de la personne void anniversaire(); //donne l'age de la personne int getAge(); //déclaration des attributs private: std::string nom; //le nom std::string prenom; //le prenom int age;//l'age };
Les fichiers .hpp sont communément appelé fichiers d'entête ("header" files).
Les classes que vous allez programmer dans le projet vont se combiner de façon relativement complexe et vous allez parfois devoir inclure de nombreux fichiers .hpp existants au début d'un nouveau fichier .hpp. Si des fichiers ont été inclus de façon redondante dans ces différents .hpp, le compilateur réagira par un message d'erreur. Pour éviter cette situation, prenez l'habitude de mettre au début de chaque fichier monfichier.hpp la directive :#pragma once
Ceci permet d'éviter les inclusions multiples. - et Personne.cpp (ou .cc)
qui contient les définitions des méthodes :
#include <iostream> #include <string> #include "Personne.hpp" // PRECAUTION 1 : INCLUSION using namespace std; // OK dans un .cc // Constructeur Personne::Personne( string nom, string prenom,int age )// <- PRECAUTION 2 : utilisation de :: :nom(nom) , prenom(prenom) , age(age) { //Rien } //affiche les infos d'une personne // Personne::afficher se lit "méthode afficher de la classe Personne" void Personne::afficher(){ cout << "Nom: " << this->nom << endl; cout << "Prenom: " << this->prenom << endl; cout << "Age: " << this->age << endl; } //incremente l'age de la personne void Personne::anniversaire(){ this->age++; cout << this->prenom << " vient de feter son " << this->age << "eme anniversaire" << endl; } //donne l'age de la personne int Personne::getAge(){ return age; }
Vous noterez les précautions à prendre pour établir le lien entre le prototype et la définition:
- on inclut Personne.hpp dans Personne.cpp;
- et on utilise l'opérateur de résolution de portée :: pour dire qu'une méthode donnée appartient à une classe donnée.
Maintenant Application.cpp , qui contient le programme principal, peut s'écrire de façon beaucoup plus concise, comme suit:
#include "Personne.hpp" int main (){ Personne stan("Wawrinka","Stan", 34); stan.afficher(); return 0; }
Nous allons maintenant nous poser la question de comment compiler et lancer ce programme qui se trouve rédigé sur trois fichiers différents.
Commencer par copier les fichiers Personne.hpp, Personne.cpp, Application.cpp TestPersonne.cpp et dans votre dossier ~/Desktop/myfiles/Programmation/cpp/serie18/exoCmake.
Compilation à la main
Il n'est pas nécessaire de réaliser la compilation «à la main» tel que décrit ci-dessous. Les explications qui suivent servent simplement à illustrer la procédure laborieuse à suivre si on avait à le faire.En ligne de commande, avec un terminal, il faudrait:- lancer la compilation du module Personne:
g++ -std=c++11 Personne.cpp -c -o Personne.o
Notez que l'option -c fait que l'on compile uniquement : on génère un fichier objet Personne.o qui n'est pas exécutable puisqu'il ne contient pas de main!
- lancer la compilation de Application:
g++ -std=c++11 Application.cpp -c -o Application.o
- Faire l'édition de lien (lier entre eux tous les .o pour obtenir un exécutable et donc ici pas d'option -c) :
g++ -std=c++11 Personne.o Application.o -o application
Parfois l'édition de lien se fait aussi avec des bibliothèques externes, on peut alors mettre ces bibliothèques en option (ici -lm pour les fonctions mathématiques):
g++ -std=c++11 -lm Personne.o Application.o -o application
On a ainsi généré un exécutable application que l'on peut lancer comme suit: ./application.
Supposons maintenant que l'on souhaite écrire un autre programme principal uniquement dédié à tester les fonctionnalités de la classe Personne.
Soit TestPersonne.cpp un exemple de tel programme :
#include <iostream> #include "Personne.hpp" using namespace std; int main() { //nouvelle instance de Personne int age = 24; Personne vincent("Dupont", "Vincent", age); vincent.anniversaire(); return 0; }
Nous nous trouvons désormais avec deux fichiers contenant un main et si l'on compile TestPersonne.cpp pour obtenir le fichier objet TestPersonne.o et que l'on fait ensuite l'édition de lien de cette façon:
g++ -std=c++11 Personne.o Application.o TestPersonne.o -o application
le compilateur signalera un souci : il y a deux main, lequel doit servir à construire l'exécutable application ??
La commande d'édition de lien correcte doit spécifier un seul main.
On écrirait donc plutôt la commande suivante pour lancer la génération de l'exécutable main_app :
g++ -std=c++11 Personne.o Application.o -o application
et celle ci-dessous pour générer l'exécutable test_personne :
g++ -std=c++11 Personne.o TestPersonne.o -o test_personne
Cet exemple nous permet de constater l'un des problèmes importants lié à la compilation/édition de lien manuelle. En effet, pour chaque main que nous voudrions exécuter, il faut choisir manuellement quels fichiers compiler, et quel exécutable il faudrait produire. D'autres désavantages de cette approche sont :
- qu'il faut écrire une nouvelle commande pour chaque exécutable;
- que sur un grand projet, il est difficile de savoir quels fichiers il faut recompiler et lesquels n'ont subi aucune modification;
- et que si les fichiers sont organisés dans différents sous-dossiers, il faut correctement spécifier tous les chemins "à la main".
Il serait donc utile d'avoir une manière plus simple et systématique construire un projet.
Organisation des fichiers
Avant d'aborder l'outil cmake qui va grandement nous simplifier la vie, réorganisons rapidement nos fichiers. C'est une pratique utile dès que l'on entreprend des projets de plus grande envergure. Faites en sorte que le dossier dans lequel vous faites cet exercice ait maintenant la forme suivante:
~/Desktop/myfiles/Programmation/cpp/serie18/exoCmake/ src/ Personne/ Personne.h Personne.cpp Programs/ Application.cpp TestPersonne.cpp
Une fois cette restructuration faite, ouvrez ces fichiers dans Geany ou dans QtCreator. Si vous utilisez ce dernier, ouvrez les fichiers sans les intégrer à un projet en faisant simplement:
Open File or Project
puis ouvrez directement le fichier concerné (donc sans passer par la création d'un projet). Faites alors abstraction du message indiquant que ces fichiers ne sont intégrés à aucun projet. Nous y remédierons un peu plus tard.Il nous faut aussi, suite à la restructuration réadapter les inclusions:
- dans Application l'inclusion
#include "Personne.hpp"
devient:#include "../Personne/Personne.hpp"
- et dans TestPersonne.cpp l'inclusion:
#include "Personne.hpp"
devient:#include "../Personne/Personne.hpp"
La structure proposée a pour but de séparer les programmes a exécuter du reste. Le dossier Personne peut sembler superflu à ce stade, mais il se peut, dans des projets plus grands, que nous souhaitions séparer des fonctionnalités en différents dossiers.
Dans la suite (pour votre projet par exemple), vous trouverez utile d'adhérer à une telle organisation de vos fichiers.
CMake
CMake est un outil de construction de projet. Il permet, à l'aide d'un fichier de description, de déclarer comment l'on veut qu'un projet soit compilé.
Il s'agit d'un outil puissant dont nous n'apprendrons, dans le cadre de ce cours, à utiliser que les bases fondamentales. Pour pousser l'apprentissage plus loin, vous pouvez consulter la documentation officielle.
Dans le répertoire ~/Desktop/myfiles/Programmation/cpp/serie18/exoCMake/src, éditez un nouveau fichier que vous nommerez CMakeLists.txt (par exemple à à l'aide de l'éditeur Geany ou dans QtCreator en utilisant New File or Project > General > Empty file).
Il est impératif que ce fichier soit nommé ainsi.
Ajoutez dans ce fichier les lignes suivantes:
cmake_minimum_required(VERSION 3.5) project(TEST_COMPILATION_SEPAREE) set(CMAKE_CXX_STANDARD 11) set(CMAKE_BUILD_TYPE Debug) set(CMAKE_CXX_FLAGS "-Wall") include_directories(${PROJECT_SOURCE_DIR}) file(GLOB PROJECT_SOURCES "${PROJECT_SOURCE_DIR}/Personne/*.cpp" "${PROJECT_SOURCE_DIR}/Personne/*.hpp" ) add_executable (application ${PROJECT_SOURCE_DIR}/Programs/Application.cpp ${PROJECT_SOURCES}) target_link_libraries(application m) add_executable (test_personne ${PROJECT_SOURCE_DIR}/Programs/TestPersonne.cpp ${PROJECT_SOURCES}) target_link_libraries(test_personne m)
Quelques explications:
- project(TEST_COMPILATION_SEPAREE) (facultatif), permet de donner un nom au projet pour lequel on souhaite construire des exécutables.
- set(CMAKE_CXX_STANDARD 11) indique que l'on veut compiler sous la norme C++11 (c'est la même chose que -std=c+11 dans la commande de compilation manuelle).
- set(CMAKE_BUILD_TYPE Debug) indique que l'on veut compiler en mode «debug» (c'est la même chose que -g dans la commande de compilation manuelle). On peut commenter cette ligne en ajoutant le caractère # (au début de cette ligne) lorsque l'on ne souhaite plus compiler en mode «debug».
- set(CMAKE_CXX_FLAGS "-Wall") permet de spécifier des options de compilations particulières (par exemple ici on veut que le compilateur signale tous les «warning»)
- include_directories(${PROJECT_SOURCE_DIR}) permet d'inclure le répertoire racine du projet (celui où se trouve le fichier CMakeLists.txt) dans les chemins où le compilateur fait sa recherche des fichiers à compiler.
-
file(GLOB PROJECT_SOURCES "${PROJECT_SOURCE_DIR}/Personne/*.cpp" "${PROJECT_SOURCE_DIR}/Personne/*.hpp" )
définit une variable d'environnement PROJECT_SOURCES qui décrit où se localisent les fichiers à compiler pour produire les exécutables. - Les deux séquences qui suivent permettent de définir des «cibles» à construire, dans notre cas :
- une cible application qui spécifie comment créer le fichier exécutable application correspondant au programme principal Application.cpp;
- une cible test_personne qui spécifie comment créer le fichier exécutable test_personne correspondant au programme principal TestPersonne.cpp;
- les lignes target_link_libraries(...) permettent de lier les exécutables à des librairies externe (c'est l'équivalent du -lm dans l'exemple de compilation manuelle donnée plus haut et n'est pas strictement nécessaire ici).
Le nom des cibles peut être librement choisi bien entendu.
Nous sommes maintenant prêts a lancer notre compilation/édition de lien au moyen de CMake!
Construction des cibles dans QtCreator
Vous pouvez maintenant créer un projet QtCreator de façon analogue à ce qui est décrit ici. Vous verrez que la commande de «build» (petit marteau) permet de créer les deux cibles, que vous pouvez tour à tour choisir pour les exécuter.
- Tout changement dans le contenu des répertoires sources et/ou dans le fichier CMakeLists.txt nécessite de rafraîchir le matériel de «build/construction», ce qui se fait au moyen de : Build > Run CMake;
- Lorsque l'on invoque la commande «build» (marteau), seuls les fichiers qui ont été modifiés (directement ou indirectement par le biais des inclusions) sont recompilés, ce qui est d'un apport précieux dans les grand projets.
- Il est possible de forcer la recompilation de tout le projet au moyen de la commande Build > Clean Project <nom_du_projet> (ce qui revient à vider le dossier build/)
Construction des cibles à la main
Sous Linux ou MacOs, il n'est pas nécessaire d'utiliser un EDI comme QTCreator pour lancer la compilation et l'exécution des cibles au moyen de CMake. Pour reproduire ce qui se passe avec QtCreator:
- créez un dossier build/ au même niveau que le dossier src/ (s'il n'existe pas)
- dans un terminal, allez dans ce répertoire build/
cd ~/Desktop/myfiles/Programmation/cpp/serie18/exoCmake/build
- lancez la construction du matériel de compilation au moyen de la commande
cmake ../src
le chemin donné en argument à la commande cmake indique où aller lire le fichier CMakeLists.txt
Vous pouvez ensuite, à votre guise, lancer l'une ou l'autre des cibles exécutables:
make application ./application
oumake test_personne ./test_personne
Pour conclure cette introduction
Cmake permet de faire bien d'avantage que ce qui est décrit ici. Par exemple, si vous regardez le fichier CmakeLists.txt fourni dans le tutoriel sur QtCreator, vous verrez qu'il contient des instructions permettant de trouver par lui même où est installée la librairie SFML et comment l'intégrer proprement aux cibles graphiques.
Comme indiqué plus haut, vous pouvez investiguer la documentation officielle pour pousser vos compétences sur cet outil plus loin. A noter que dans le cadre du projet, l'introduction qui vous est présentée ici est suffisante.
Notez enfin que Cmake utilise un outil de plus bas niveau appelé Make. C'est la raison pour laquelle la commande cmake produit un fichier Makefile dans le dossier build. Il est possible d'utiliser directement Make comme outil de compilation séparée (voir à ce propos ce tutoriel du cours de C++ des sections MA/PH).
Retour à la série
Dernière mise à jour : 2022/03/10 10:50:22