Gestion des headers ✱

Rappel

La génération d’un programme (build) consiste à transformer des codes sources au format texte en programme exécutable pour une machine donnée. Ce processus repose sur deux actions :

  • La compilation transforme chaque fichier source (.cpp) en un fichier objet.

  • La liaison (link) fusionne les différents fichiers objets pour construire le programme exécutable final.

La compilation séparée désigne le fait de compiler chaque fichier source (.cpp) séparément. Ainsi, lors de la compilation d’un fichier source (.cpp), le compilateur n’a aucune connaissance du contenu présent dans les autres fichiers sources (.cpp). Cette approche présente plusieurs avantages :

  • Une meilleure gestion des projets : chaque fichier est dédié à une classe / thématique particulière.

  • Réduction du temps de compilation par parallélisation.

  • Faciliter la maintenance et le débogage du code.

Note

La compilation se déclenche pour chaque fichier source (.cpp) par pour les fichiers d’entête (.h).

Organisation

// EXPORT DE FONCTION
// fichier : fnt.h                              // fichier : fnt.cpp

#pragma once                                    #include "fnt.h"

// déclaration                                  //définition
int fnt(...);                                   int fnt(...)
                                                {
                                                   ...
                                                }

///////////////////////////////////////////////////////////////////////////////////

// EXPORT D'UNE CLASSE - STYLE JAVA
// fichier : maclasse.h

#pragma once

class T
{
    int fnt(...) { instructions }
};

///////////////////////////////////////////////////////////////////////////////////

// EXPORT D'UNE CLASSE - STYLE C++
// fichier : maclasse.h                          // fichier : maclasse.cpp

#pragma once                                     #include "maclasse.h"

// définition de la classe                       // définition des fonctions membres
class T                                          T::fnt(...)
{                                                {
    int fnt(...);                                   instructions
};                                               }

///////////////////////////////////////////////////////////////////////////////////

// EXPORT D'UNE FONCTION TEMPLATE
// fichier : templatefnt.h

#pragma once

template<typename T>
T max(T a, T b)
{
   if (b>a) return b; else return a;
}

Note

Lorsque le compilateur lit une directive #include, il substitue cette ligne par le contenu du fichier header. Le compilateur traite ainsi ces lignes comme si elles appartenaient au fichier source. Si ces nouvelles lignes incluent à leur tour d’autres directives #include alors le mécanisme de substitution se poursuit.

Usage :

  • Chaque fichier source maclasse.cpp doit inclure son fichier header : #include "maclasse.h"

  • Chaque fichier header doit inclure la directive #pragma once

  • Lorsqu’un fichier (.h ou .cpp) a besoin d’un élément, il inclut le fichier header associé

  • Normalement, aucun fichier (.h ou /.cpp) n’inclut un fichier source (.cpp)

Les problèmes

La directive #pragma once désactive toute tentative de réinclusion. Cela permet d’optimiser le temps de compilation mais pas uniquement comme nous allons le voir ci-dessous :

Inclusion multiple

Supposons que nous ayons un header V2.h inclus dans deux autres headers collision.h et path.h. Jusque là, aucun soucis. Un fichier source eleve.cpp inclut le header collision.h et le header path.h. En jouant le jeu des substitutions des #include, le contenu de V2.h est finalement copié-collé deux fois dans eleve.cpp.

../_images/double.png

Que risque-t-on ?

  • Pour les fonctions, le fait d’avoir plusieurs déclarations dans le même fichier source ne pose aucun problème.

  • Pour les structures et les fonctions templates, la règle ODR (One Definition Rule) fera des dégâts car le compilateur va émettre une erreur de redéfinition.

Heureusement pour nous, la directive #pragma once règle ce problème car elle va bloquer la réinclusion d’un même header et donc éviter les problèmes de redéfinition.

Avertissement

Il faut écrire les directives #pragma once sur la première ligne de chaque fichier header.

Dépendance cyclique

../_images/cycle.png

Ce scénario arrive lorque

  • Le header A.h inclut le header B.h

  • Le header B.h inclut le header A.h

En compilant le fichier source A.cpp, le compilateur va naturellement trouver l’inclusion du header A.h qu’il va copier-coller à l’intérieur du code source. Ensuite, en parcourant les nouvelles lignes issues du header A.h, le compilateur va trouver l’inclusion du header B.h et copier-coller son contenu dans A.cpp. Mais le contenu du header de B.h incluant le contenu de A.h, on ne va jamais sortir du cycle ce qui va finir par lever erreur de compilation.

Avertissement

Cette situation est rare mais vous pourrez la rencontrer. Il n’y a pas de miracle cette fois, aucune directive magique à l’horizon. Il faudra revoir la conception de votre programme et l’organisation de vos fichiers pour éviter cette situation.

Définir une fonction dans un header

Nous l’avons déjà dit et il faut absolument l’éviter : définir une fonction dans un header. Mais pourquoi cela pose-t-il problème ? Après tout, la directive pragma once est là pour empêcher toute ré-inclusion et donc il ne peut y avoir de redéfinition de la même fonction durant l’étape de compilation ! Oui, c’est en partie vrai. Mais il existe une autre configuration. Prenons un exemple : supposons que par inadvertance, nous ayons malencontreusement défini une fonction test() dans le fichier header test.h. Lors de la compilation du fichier source A.cpp, cette fonction est définie une seule fois et donne naissance à la fonction void test(void) dans le fichier A.o, pas d’erreur de ce côté. Il en est de même lors de la compilation du fichier source B.cpp, une seule définition d’une fonction test qui donne naissance à la fonction void test(void) dans le fichier B.o. Seulement, au moment de la liaison, le linker se retrouve avec deux fonctions, une dans A.o et l’autre dans B.o avec exactement le même propotype : void test(void) et à ce moment là, il émet une erreur de double définition.

../_images/definheader.jpg

Comme les pros

Voici un schéma qui présente une chaîne de compilation complète. Chaque fichier source (.cpp) déclencher une compilation. On remarques que ces fichiers source incluent différents headers, mais toujours sans créer de cycle. L’étape de compilation construit des fichiers .o qui correspondent à des morceaux du programme final. Ils seront ensuite liés pour construire un exécutable.

Tout projet professionnel inclut des librairies annexes : 3D, IHM, BDD,… Ces librairies ne publient pas leurs codes sources, ceci pour diverses raisons, notamment pour préserver leur propriété intellectuelle. Ainsi, chaque librairie fournit :

  • Des headers contenant les fonctions/classes exportées par la librairie.

  • Des fichiers .lib équivalents aux fichier .o (fichiers .cpp compilés) masquant le code source.

Le linker dispose ainsi de toutes les informations nécessaires pour construire l’exécutable final :

../_images/compil.png

Quizzz

  • L’étape de liaison se déroule avant l’étape de compilation.

  • La compilation séparée permet de compiler les fichiers source indépendamment.

  • L’étape de compilation consiste à compiler chaque fichier source.

  • On doit définir les fonctions dans les fichiers entête.

  • Le compilateur sait résoudre les dépendances cycliques.

  • La directive #pragma once se rencontre dans les fichiers d’entête.

  • Si l’on inclut plusieurs fois le même fichier header dans un même fichier source, cela déclenche une erreur de liaison.

  • Une librairie externe, si elle ne fournit pas de fichiers source, propose un fichier précompilé (.lib) à la place ?

  • Il est d’usage qu’un fichier source inclut son propre fichier d’entête.

  • En C++, il est possible, comme en Java, de donner les corps des fonctions membres d’une classe directement dans le header.