Licence CC BY-NC

Découpons du code — Les fichiers

Dernière mise à jour :

Lors du chapitre précédent, nous avons codé notre premier programme d’assez grande ampleur. Et avec l’ampleur que prend notre code, vient un problème qui était caché par la taille réduite de ce qu’on avait fait jusqu’alors : l’organisation du code.

En effet, jusqu’à maintenant, on mettait simplement tout notre code dans le fichier main.cpp. Le plus sophistiqué qu’on ait fait en termes d’organisation est d’utiliser les prototypes pour pouvoir reléguer les implémentations après la fonction main et ainsi alléger un peu la lecture du code.

Jusque-là ça suffisait, mais on voit déjà avec le TP que dès que le projet prend un peu d’ampleur, ça devient un sacré fouillis. Et encore, ça n’est rien comparé à certains gros projets. Par exemple, à l’heure où j’écris ces lignes, le code du noyau Linux comporte 17 millions de lignes !

C’est pourquoi on va maintenant apprendre comment séparer son code en plusieurs fichiers, ce qui va nous permettre de mieux l’organiser.

Le principe

Nous savons déjà séparer les prototypes de leur implémentation, ce qui est déjà une première étape vers une réorganisation. Mais ce n’est pas suffisant. Il faut maintenant séparer le code en plusieurs fichiers. Et il en existe deux types.

Le fichier d’en-tête

Les fichiers d’en-tête contiennent les déclarations des types et fonctions que l’on souhaite créer. Nous sommes habitués à les manipuler depuis le début de notre aventure avec C++. C’est grâce à eux que nous sommes en mesure d’utiliser la bibliothèque standard. Le concept n’est pas nouveau, on passe simplement de consommateur à créateur.

Les fichiers d’en-tête peuvent tout à fait inclure d’autres fichiers d’en-tête, tant standards que personnels. C’est le cas par exemple où notre fichier d’en-tête fait appel à des composants de la bibliothèque standard, comme, entre autres, std::string ou std::vector.

Sur Visual Studio

Ouvrez votre projet avec Visual Studio. Vous tombez sur un écran semblable au mien.

Page principale de Visual Studio avant l'ajout d'un fichier d'en-tête au projet.
Page principale de Visual Studio avant l’ajout d’un fichier d’en-tête au projet.

Faîtes un clic-droit sur le nom du projet (ici, TestCpp), puis cliquez sur Ajouter -> Nouvel élément…. Ce faisant, vous arrivez sur l’écran suivant.

Choisissez bien Fichier d'en-tête (.h)
Choisissez bien « Fichier d’en-tête (.h) ».

Choisissez bien Fichier d’en-tête (.h) puis donnez un nom à votre fichier. Ici, j’ai choisi test.hpp. Notez que j’ai changé l’extension, de .h vers .hpp, et je vous invite à faire de même. Enfin, cliquez sur Ajouter. Vous arriverez sur l’écran principal, avec le fichier ajouté au projet.

Le fichier d'en-tête a bien été ajouté.
Le fichier d’en-tête a bien été ajouté.

Sur QtCreator

Ouvrez votre projet avec Qt Creator, ce qui devrait vous donner quelque chose comme moi.

Page principale de Qt Creator avant l'ajout d'un fichier d'en-tête au projet.
Page principale de Qt Creator avant l’ajout d’un fichier d’en-tête au projet.

Faîtes un clic-droit sur le nom du projet (ici, ZdSCpp), puis cliquez sur Add new…. Vous tombez sur la fenêtre suivante.

Il faut choisir C++ Header File.
Il faut choisir « C++ Header File ».

Cliquez sur C++ Header File, puis sur Choose… en bas à droite. Vous passez au deuxième écran.

Il faut maintenant choisir un nom.
Il faut maintenant choisir un nom.

Maintenant, donnez un nom à votre fichier. Ici, pour l’exemple, je l’ai appelé test.hpp. Cliquez ensuite sur Suivant et vous arriverez sur le troisième écran.

Normalement, vous n'avez rien besoin de toucher.
Normalement, vous n’avez rien besoin de toucher.

Tout est bon, il ne reste plus qu’à cliquer sur Terminer et nous en avons fini avec l’ajout de fichiers d’en-tête. On retombe sur la page principale, avec notre fichier bien présent en haut à gauche, signe qu’il a bien été ajouté au projet.

Qt Creator Page principale après. Voilà, tout est bon.
Voilà, tout est bon.

En ligne de commande

Rien de compliqué, il suffit de créer un simple fichier .hpp et c’est tout. :)

Le fichier source

Le fichier source va contenir les implémentations de nos fonctions. La création d’un fichier source est exactement identique à celle de la création d’un fichier d’en-tête, à ceci prêt que, cette fois, c’est un fichier source. ;)

Un fichier source peut inclure autant de fichiers d’en-tête que nécessaire. Il y a bien sûr celui qui contient les prototypes et les types qu’il définit, mais il peut en inclure d’autres.

Créons un lien

Il reste une étape importante : lier les fichiers d’en-têtes et sources. Pour cela, il suffit d’inclure le fichier d’en-tête concerné dans le fichier source concerné. Cependant, à la différence des fichiers de la bibliothèque standard qui s’incluent avec des chevrons, on utilise les guillemets doubles ". On doit aussi écrire le nom complet, extension comprise. Celle-ci est .hpp.

Ainsi, si vous nommez votre fichier test.hpp, vous l’inclurez dans le code en faisant comme suit.

#include "test.hpp"
Extension

Les fichiers d’en-tête peuvent aussi utiliser l’extension .h. C’est parfaitement valide. La différence n’est qu’une histoire de goût. Nous préférons .hpp à .h par analogie aux fichiers sources, qui se terminent par .cpp en C++ mais .c en C.

Voici un exemple tout bête mais fonctionnel de séparation en plusieurs fichiers. Pour le reproduire, créez les fichiers concernés dans votre projet et copiez-collez les codes ci-dessous.

test.hpp
// Parce que le compilateur a besoin de savoir ce qu'est std::string.
#include <string>

// On ne met ici que le prototype.
void afficher_message(std::string const & message);
test.cpp
// On a besoin de iostream pour utiliser std::cout, std::quoted et std::endl.
#include <iostream>
// On inclut le fichier d'en-tête correspondant.
#include "test.hpp"

// Par contre, comme 'test.hpp' inclut déjà '<string>', on n'a pas besoin de l'inclure de nouveau ici, dans le fichier source.

// On définit la fonction ici.
void afficher_message(std::string const & message)
{
    std::cout << "J'ai reçu un message : " << std::quoted(message) << std::endl;
}
main.cpp
// Pour appeler la fonction, il faut connaître son prototype, donc on doit inclure le fichier d'en-tête correspondant.
#include "test.hpp"

int main()
{
    // Ici, on va utiliser la fonction.
    afficher_message("Salut les lecteurs !");
    return 0;
}

Pour compiler ce programme avec Visual Studio ou Qt Creator, rien de bien compliqué, on lance la compilation et c’est tout. Pour ceux qui sont en console, il faut ajouter chaque fichier .cpp à la liste des fichiers à compiler. Quant aux .hpp, il ne faut pas les inclure, nous verrons pourquoi plus tard.

L’exemple suivant se compile ainsi.

> g++ -std=c++17 test.cpp main.cpp -o programme.out
> clang++ -std=c++17 test.cpp main.cpp -o programme.out

La sécurité, c'est important

Avez-vous noté ces lignes nouvelles qui étaient automatiquement présentes lors de la création du fichier d’en-tête ? Ce sont surtout celles de Qt Creator qui nous intéressent (celle de Visual Studio n’est pas du C++ standard). Je les réécris ci-dessous.

#ifndef TEST_HPP
#define TEST_HPP

#endif // TEST_HPP

C’est qu’en fait, lorsque le compilateur voit une ligne #include, il ne réfléchit pas et inclut le fichier en question. Sauf que, si, par erreur, ou même volontairement, on inclut plusieurs fois le même fichier, on va se retrouver avec plusieurs fois les mêmes déclarations de fonctions, de types, etc. Autant dire que le compilateur ne va que très moyennement apprécier.

Afin de s’en prémunir, il faut écrire ces trois lignes, qui sont des directives de préprocesseur et ce dans chaque fichier d’en-tête que vous créerez. Vous pouvez remplacer TEST_HPP par ce que vous voulez, à condition que ce nom soit unique. Le plus simple est de le remplacer par le nom du fichier en question : s’il s’appelle autre.hpp, alors écrivez AUTRE_HPP. Le code de l’en-tête ira entre la deuxième et la troisième ligne.

Ainsi, le fichier d’en-tête de l’exemple de la section précédente n’est pas correct. Il faut le corriger en utilisant ce que nous venons d’apprendre.

#ifndef TEST_HPP
#define TEST_HPP

#include <string>

void afficher_message(std::string const & message);

#endif

Découpons le TP !

Pour s’exercer, on va prendre le corrigé du TP précédent et on va l’organiser en plusieurs fichiers. Commençons par le commencement, séparons prototypes et implémentations. On aura alors l’esprit plus clair pour réorganiser tout ça une fois qu’on aura uniquement les prototypes sous les yeux.

Séparation prototype / implémentation
#include <algorithm>
#include <cassert>
#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>

std::string traitement_chaine(std::string const & chaine);

struct Artiste
{
    std::string nom;
};

std::ostream & operator<<(std::ostream & sortie, Artiste const & artiste);

struct Album
{
    std::string nom;
};

std::ostream & operator<<(std::ostream & sortie, Album const & album);

struct Morceau
{
    std::string nom;
    Artiste compositeur;
    Album album;
};

std::ostream & operator<<(std::ostream & sortie, Morceau const & morceau);
std::istream & operator>>(std::istream & entree, Morceau & morceau);

void test_creation_morceau_entree_complete();
void test_creation_morceau_entree_espaces_partout();
void test_creation_morceau_entree_chanson_artiste();
void test_creation_morceau_entree_chanson_uniquement();
void test_creation_morceau_entree_vide();

using Discographie = std::vector<Morceau>;

void tri_morceau(Discographie& discographie);
void tri_album(Discographie& discographie);
void tri_artiste(Discographie& discographie);

void affichage_morceau(Discographie const & discographie);
void affichage_album(Discographie const & discographie);
void affichage_artiste(Discographie const & discographie);

enum class Affichage { Artiste, Album, Morceau };

void affichage(Discographie& discographie, Affichage type_affichage);

void enregistrement(Discographie const & discographie, std::string const & nom_fichier);
void chargement(Discographie & discographie, std::string const & nom_fichier);

std::string recuperer_commande();

enum class Commande { Afficher, Ajouter, Enregistrer, Charger, Quitter };

std::tuple<Commande, std::string> analyser_commande(std::string const & commande_texte);
bool executer_commande(Discographie & discographie, Commande commande, std::string const & instructions);

int main()
{
    test_creation_morceau_entree_complete();
    test_creation_morceau_entree_espaces_partout();
    test_creation_morceau_entree_chanson_artiste();
    test_creation_morceau_entree_chanson_uniquement();
    test_creation_morceau_entree_vide();

    Artiste const dave_brubeck { "Dave Brubeck " };
    Artiste const secret_garden { "Secret Garden" };
    Artiste const indochine { "Indochine" };

    Album const time_out { "Time Out" };
    Album const songs_from_a_secret_garden { "Songs from a Secret Garden" };
    Album const l_aventurier { "L'aventurier" };
    Album const paradize { "Paradize" };

    Morceau const take_five { "Take Five", dave_brubeck, time_out };
    Morceau const blue_rondo_a_la_turk { "Blue Rondo a la Turk", dave_brubeck, time_out };
    Morceau const nocturne { "Nocturne", secret_garden, songs_from_a_secret_garden };
    Morceau const aventurier { "L'aventurier", indochine, l_aventurier };
    Morceau const j_ai_demande_a_la_lune { "J'ai demandé à la lune", indochine, paradize };

    Discographie discographie { take_five, blue_rondo_a_la_turk, nocturne, aventurier, j_ai_demande_a_la_lune };

    bool continuer { true };
    do
    {
        try
        {
            std::string entree { recuperer_commande() };
            auto[commande, instructions] = analyser_commande(entree);
            instructions = traitement_chaine(instructions);
            continuer = executer_commande(discographie, commande, instructions);
        }
        catch (std::runtime_error const & exception)
        {
            std::cout << "Erreur : " << exception.what() << std::endl;
        }

    } while (continuer);

    return 0;
}

std::string traitement_chaine(std::string const & chaine)
{
    std::string copie { chaine };

    auto premier_non_espace { std::find_if_not(std::begin(copie), std::end(copie), isspace) };
    copie.erase(std::begin(copie), premier_non_espace);

    std::reverse(std::begin(copie), std::end(copie));
    premier_non_espace = std::find_if_not(std::begin(copie), std::end(copie), isspace);
    copie.erase(std::begin(copie), premier_non_espace);
    std::reverse(std::begin(copie), std::end(copie));

    return copie;
}

std::ostream & operator<<(std::ostream & sortie, Artiste const & artiste)
{
    sortie << artiste.nom;
    return sortie;
}

std::ostream & operator<<(std::ostream & sortie, Album const & album)
{
    sortie << album.nom;
    return sortie;
}

std::ostream & operator<<(std::ostream & sortie, Morceau const & morceau)
{
    sortie << morceau.nom << " | " << morceau.album << " | " << morceau.compositeur;
    return sortie;
}

std::istream & operator>>(std::istream & entree, Morceau & morceau)
{
    std::string mot {};
    std::ostringstream flux {};

    while (entree >> mot && mot != "|")
    {
        flux << mot << " ";
    }

    std::string nom_morceau { flux.str() };
    if (std::empty(nom_morceau))
    {
        nom_morceau = "Morceau inconnu";
    }
    morceau.nom = traitement_chaine(nom_morceau);
    flux.str(std::string {});

    while (entree >> mot && mot != "|")
    {
        flux << mot << " ";
    }

    std::string nom_album { flux.str() };
    if (std::empty(nom_album))
    {
        nom_album = "Album inconnu";
    }
    morceau.album.nom = traitement_chaine(nom_album);
    flux.str(std::string {});

    while (entree >> mot)
    {
        flux << mot << " ";
    }

    std::string nom_artiste { flux.str() };
    if (std::empty(nom_artiste))
    {
        nom_artiste = "Artiste inconnu";
    }
    morceau.compositeur.nom = traitement_chaine(nom_artiste);
    flux.str(std::string {});

    return entree;
}

void test_creation_morceau_entree_complete()
{
    std::istringstream entree { "Frica | Frica | Carla's Dreams" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Frica" && u8"Le nom du morceau doit être Frica.");
    assert(morceau.album.nom == "Frica" && u8"Le nom de l'album doit être Frica.");
    assert(morceau.compositeur.nom == "Carla's Dreams" && u8"Le nom du compositeur doit être Carla's Dreams.");
}

void test_creation_morceau_entree_espaces_partout()
{
    std::istringstream entree { "Levels       |  Levels   |     Avicii" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Levels" && u8"Le nom du morceau doit être Levels.");
    assert(morceau.album.nom == "Levels" && u8"Le nom de l'album doit être Levels.");
    assert(morceau.compositeur.nom == "Avicii" && u8"Le nom du compositeur doit être Avicii.");
}

void test_creation_morceau_entree_chanson_artiste()
{
    std::istringstream entree { "Subeme la radio | | Enrique Iglesias" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Subeme la radio" && u8"Le nom du morceau doit être Subeme la radio.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Enrique Iglesias" && u8"Le nom du compositeur doit être Enrique Iglesias.");
}

void test_creation_morceau_entree_chanson_uniquement()
{
    std::istringstream entree { "Duel of the fates | |" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Duel of the fates" && u8"Le nom du morceau doit être Duel of the Fates.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Artiste inconnu" && u8"Le nom du compositeur doit être Artiste inconnu.");
}

void test_creation_morceau_entree_vide()
{
    std::istringstream entree { "| |" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Morceau inconnu" && u8"Le nom du morceau doit être Morceau inconnu.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Artiste inconnu" && u8"Le nom du compositeur doit être Artiste inconnu.");
}

void tri_morceau(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        return lhs.nom < rhs.nom;
    });
}

void tri_album(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        if (lhs.album.nom < rhs.album.nom)
            return true;

        return lhs.album.nom == rhs.album.nom && lhs.nom < rhs.nom;
    });
}

void tri_artiste(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        if (lhs.compositeur.nom < rhs.compositeur.nom)
            return true;

        if (lhs.compositeur.nom == rhs.compositeur.nom)
        {
            if (lhs.album.nom < rhs.album.nom)
                return true;

            return lhs.album.nom == rhs.album.nom && lhs.nom < rhs.nom;
        }

        return false;
    });
}

// Affichage de la discographie par morceau, dans l'ordre alphabétique.
void affichage_morceau(Discographie const & discographie)
{
    for (Morceau const & morceau : discographie)
    {
        std::cout << "--> " << morceau << std::endl;
    }
}

// Affichage de la discographie par album, dans l'ordre alphabétique.
void affichage_album(Discographie const & discographie)
{
    Album album_precedent{};
    for (Morceau const & morceau : discographie)
    {
        if (morceau.album.nom != album_precedent.nom)
        {
            std::cout << "--> " << morceau.album << " | " << morceau.compositeur << std::endl;
        }
        std::cout << "\t/--> " << morceau.nom << std::endl;

        album_precedent = morceau.album;
    }
}

// Affichage de la discographie par artiste, dans l'ordre alphabétique.
void affichage_artiste(Discographie const & discographie)
{
    Artiste artiste_precedent{};
    Album album_precedent{};

    for (Morceau const & morceau : discographie)
    {
        if (morceau.compositeur.nom != artiste_precedent.nom)
        {
            std::cout << "--> " << morceau.compositeur << std::endl;
        }

        if (morceau.album.nom != album_precedent.nom)
        {
            std::cout << "\t/--> " << morceau.album << std::endl;
        }

        std::cout << "\t\t/--> " << morceau.nom << std::endl;

        artiste_precedent = morceau.compositeur;
        album_precedent = morceau.album;
    }
}

void affichage(Discographie& discographie, Affichage type_affichage)
{
    if (type_affichage == Affichage::Album)
    {
        tri_album(discographie);
        affichage_album(discographie);
    }
    else if (type_affichage == Affichage::Artiste)
    {
        tri_artiste(discographie);
        affichage_artiste(discographie);
    }
    else if (type_affichage == Affichage::Morceau)
    {
        tri_morceau(discographie);
        affichage_morceau(discographie);
    }
    else
    {
        // Par exemple si on met à jour l'énumération mais qu'on oublie d'ajouter la fonction correspondante.
        throw std::runtime_error("Commande d'affichage inconnue.");
    }
}

void enregistrement(Discographie const & discographie, std::string const & nom_fichier)
{
    std::ofstream fichier { nom_fichier };
    if (!fichier)
    {
        throw std::runtime_error("Impossible d'ouvrir le fichier en écriture.");
    }

    for (Morceau const & morceau : discographie)
    {
        fichier << morceau << std::endl;
    }
}

void chargement(Discographie & discographie, std::string const & nom_fichier)
{
    std::ifstream fichier { nom_fichier };
    if (!fichier)
    {
        throw std::runtime_error("Impossible d'ouvrir le fichier en lecture.");
    }

    std::string ligne {};
    while (std::getline(fichier, ligne))
    {
        Morceau morceau {};

        std::istringstream flux { ligne };
        flux >> morceau;

        discographie.push_back(morceau);
    }
}

std::string recuperer_commande()
{
    std::cout << "> ";

    std::string commande {};
    std::getline(std::cin, commande);
    return commande;
}

std::tuple<Commande, std::string> analyser_commande(std::string const & commande_texte)
{
    std::istringstream flux { commande_texte };

    std::string premier_mot {};
    flux >> premier_mot;
    premier_mot = traitement_chaine(premier_mot);

    std::string instructions {};
    std::getline(flux, instructions);
    instructions = traitement_chaine(instructions);

    if (premier_mot == "afficher")
    {
        return { Commande::Afficher, instructions };
    }
    else if (premier_mot == "ajouter")
    {
        return { Commande::Ajouter, instructions };
    }
    else if (premier_mot == "enregistrer")
    {
        return{ Commande::Enregistrer, instructions };
    }
    else if (premier_mot == "charger")
    {
        return { Commande::Charger, instructions };
    }
    else if (premier_mot == "quitter")
    {
        return { Commande::Quitter, std::string {} };
    }
    else
    {
        throw std::runtime_error("Commande invalide.");
    }
}

bool executer_commande(Discographie & discographie, Commande commande, std::string const & instructions)
{
    if (commande == Commande::Afficher)
    {
        if (instructions == "artistes")
        {
            affichage(discographie, Affichage::Artiste);
        }
        else if (instructions == "albums")
        {
            affichage(discographie, Affichage::Album);
        }
        else if (instructions == "morceaux")
        {
            affichage(discographie, Affichage::Morceau);
        }
        else
        {
            throw std::runtime_error("Commande d'affichage inconnue.");
        }
    }
    else if (commande == Commande::Ajouter)
    {
        std::istringstream flux { instructions };
        Morceau morceau {};
        flux >> morceau;
        discographie.push_back(morceau);
    }
    else if (commande == Commande::Charger)
    {
        chargement(discographie, instructions);
    }
    else if (commande == Commande::Enregistrer)
    {
        enregistrement(discographie, instructions);
    }
    else if (commande == Commande::Quitter)
    {
        return false;
    }
 
    return true;
}

On peut former des groupes de types et fonctions liées.

  • Les fonctions utilitaires : pour l’instant, on n’a que traitement_chaine, mais elle pourrait être rejointe par d’autres lors de l’évolution du projet.
  • Les types de base Artiste, Album et Morceau et les fonctions les manipulant.
  • Les tests unitaires correspondants.
  • Le type Discographie et ses fonctions de manipulation.
  • Les types et fonctions concernant l’exécution des commandes.

On va donc créer cinq paires de fichiers pour réorganiser notre code en conséquence : utils, donnees_disco, donnees_disco_tests, discographie, systeme_commandes.

Solution

Comme d’habitude, il n’y a pas qu’une seule solution en programmation. L’organisation que je propose en est une, mais l’essentiel est juste d’avoir un code organisé de manière cohérente.

Cela donne donc les fichiers suivants.

Utilitaire
/utils.hpp
#ifndef UTILS_HPP_INCLUDED
#define UTILS_HPP_INCLUDED

#include <string>

std::string traitement_chaine(std::string const & chaine);

#endif
/utils.cpp
#include "utils.hpp"

#include <algorithm>

std::string traitement_chaine(std::string const & chaine)
{
    std::string copie { chaine };

    auto premier_non_espace { std::find_if_not(std::begin(copie), std::end(copie), isspace) };
    copie.erase(std::begin(copie), premier_non_espace);

    std::reverse(std::begin(copie), std::end(copie));
    premier_non_espace = std::find_if_not(std::begin(copie), std::end(copie), isspace);
    copie.erase(std::begin(copie), premier_non_espace);
    std::reverse(std::begin(copie), std::end(copie));

    return copie;
}
Données discographie
/donnees_disco.cpp
#ifndef DONNEES_DISCO_HPP_INCLUDED
#define DONNEES_DISCO_HPP_INCLUDED

#include <string>
#include <iostream>
#include <vector>

struct Artiste
{
    std::string nom;
};

std::ostream & operator<<(std::ostream & sortie, Artiste const & artiste);

struct Album
{
    std::string nom;
};

std::ostream & operator<<(std::ostream & sortie, Album const & album);

struct Morceau
{
    std::string nom;
    Artiste compositeur;
    Album album;
};

std::ostream & operator<<(std::ostream & sortie, Morceau const & morceau);
std::istream & operator>>(std::istream & entree, Morceau & morceau);

using Discographie = std::vector<Morceau>;

#endif
/donnees_disco.cpp
#include "donnees_disco.hpp"

#include "utils.hpp"
#include <sstream>

std::ostream & operator<<(std::ostream & sortie, Artiste const & artiste)
{
    sortie << artiste.nom;
    return sortie;
}

std::ostream & operator<<(std::ostream & sortie, Album const & album)
{
    sortie << album.nom;
    return sortie;
}

std::ostream & operator<<(std::ostream & sortie, Morceau const & morceau)
{
    sortie << morceau.nom << " | " << morceau.album << " | " << morceau.compositeur;
    return sortie;
}

std::istream & operator>>(std::istream & entree, Morceau & morceau)
{
    std::string mot {};
    std::ostringstream flux {};

    while (entree >> mot && mot != "|")
    {
        flux << mot << " ";
    }

    std::string nom_morceau { flux.str() };
    if (std::empty(nom_morceau))
    {
        nom_morceau = "Morceau inconnu";
    }
    morceau.nom = traitement_chaine(nom_morceau);
    flux.str(std::string {});

    while (entree >> mot && mot != "|")
    {
        flux << mot << " ";
    }

    std::string nom_album { flux.str() };
    if (std::empty(nom_album))
    {
        nom_album = "Album inconnu";
    }
    morceau.album.nom = traitement_chaine(nom_album);
    flux.str(std::string {});

    while (entree >> mot)
    {
        flux << mot << " ";
    }

    std::string nom_artiste { flux.str() };
    if (std::empty(nom_artiste))
    {
        nom_artiste = "Artiste inconnu";
    }
    morceau.compositeur.nom = traitement_chaine(nom_artiste);
    flux.str(std::string {});

    return entree;
}
Tests unitaires
/donnees_disco_tests.hpp
#ifndef DONNEES_DISCO_TESTS_HPP_INCLUDED
#define DONNEES_DISCO_TESTS_HPP_INCLUDED

void test_creation_morceau_entree_complete();
void test_creation_morceau_entree_espaces_partout();
void test_creation_morceau_entree_chanson_artiste();
void test_creation_morceau_entree_chanson_uniquement();
void test_creation_morceau_entree_vide();

#endif
/donnees_disco_tests.cpp
#include "donnees_disco_tests.hpp"

#include <sstream>
#include <cassert>
#include "donnees_disco.hpp"

void test_creation_morceau_entree_complete()
{
    std::istringstream entree { "Frica | Frica | Carla's Dreams" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Frica" && u8"Le nom du morceau doit être Frica.");
    assert(morceau.album.nom == "Frica" && u8"Le nom de l'album doit être Frica.");
    assert(morceau.compositeur.nom == "Carla's Dreams" && u8"Le nom du compositeur doit être Carla's Dreams.");
}

void test_creation_morceau_entree_espaces_partout()
{
    std::istringstream entree { "Levels      |  Levels     |  Avicii" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Levels" && u8"Le nom du morceau doit être Levels.");
    assert(morceau.album.nom == "Levels" && u8"Le nom de l'album doit être Levels.");
    assert(morceau.compositeur.nom == "Avicii" && u8"Le nom du compositeur doit être Avicii.");
}

void test_creation_morceau_entree_chanson_artiste()
{
    std::istringstream entree { "Subeme la radio | | Enrique Iglesias" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Subeme la radio" && u8"Le nom du morceau doit être Subeme la radio.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Enrique Iglesias" && u8"Le nom du compositeur doit être Enrique Iglesias.");
}

void test_creation_morceau_entree_chanson_uniquement()
{
    std::istringstream entree { "Duel of the fates | |" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Duel of the fates" && u8"Le nom du morceau doit être Duel of the Fates.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Artiste inconnu" && u8"Le nom du compositeur doit être Artiste inconnu.");
}

void test_creation_morceau_entree_vide()
{
    std::istringstream entree { "| |" };
    Morceau morceau {};

    entree >> morceau;

    assert(morceau.nom == "Morceau inconnu" && u8"Le nom du morceau doit être Morceau inconnu.");
    assert(morceau.album.nom == "Album inconnu" && u8"Le nom de l'album doit être Album inconnu.");
    assert(morceau.compositeur.nom == "Artiste inconnu" && u8"Le nom du compositeur doit être Artiste inconnu.");
}
Discographie
/discographie.hpp
#ifndef DISCOGRAPHIE_HPP_INCLUDED
#define DISCOGRAPHIE_HPP_INCLUDED

#include "donnees_disco.hpp"
#include <string>

void tri_morceau(Discographie& discographie);
void tri_album(Discographie& discographie);
void tri_artiste(Discographie& discographie);

void affichage_morceau(Discographie const & discographie);
void affichage_album(Discographie const & discographie);
void affichage_artiste(Discographie const & discographie);

enum class Affichage { Artiste, Album, Morceau };

void affichage(Discographie& discographie, Affichage type_affichage);

void enregistrement(Discographie const & discographie, std::string const & nom_fichier);
void chargement(Discographie & discographie, std::string const & nom_fichier);

#endif
/discographie.cpp
#include "discographie.hpp"

#include <algorithm>
#include <fstream>
#include <sstream>
#include <stdexcept>

void tri_morceau(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        return lhs.nom < rhs.nom;
    });
}

void tri_album(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        if (lhs.album.nom < rhs.album.nom)
            return true;

        return lhs.album.nom == rhs.album.nom && lhs.nom < rhs.nom;
    });
}

void tri_artiste(Discographie& discographie)
{
    std::sort(std::begin(discographie), std::end(discographie), [](Morceau const & lhs, Morceau const & rhs)
    {
        if (lhs.compositeur.nom < rhs.compositeur.nom)
            return true;

        if (lhs.compositeur.nom == rhs.compositeur.nom)
        {
            if (lhs.album.nom < rhs.album.nom)
                return true;

            return lhs.album.nom == rhs.album.nom && lhs.nom < rhs.nom;
        }

        return false;
    });
}

// Affichage de la discographie par morceau, dans l'ordre alphabétique.
void affichage_morceau(Discographie const & discographie)
{
    for (Morceau const & morceau : discographie)
    {
        std::cout << "--> " << morceau << std::endl;
    }
}

// Affichage de la discographie par album, dans l'ordre alphabétique.
void affichage_album(Discographie const & discographie)
{
    Album album_precedent{};
    for (Morceau const & morceau : discographie)
    {
        if (morceau.album.nom != album_precedent.nom)
        {
            std::cout << "--> " << morceau.album << " | " << morceau.compositeur << std::endl;
        }
        std::cout << "\t/--> " << morceau.nom << std::endl;

        album_precedent = morceau.album;
    }
}

// Affichage de la discographie par artiste, dans l'ordre alphabétique.
void affichage_artiste(Discographie const & discographie)
{
    Artiste artiste_precedent{};
    Album album_precedent{};

    for (Morceau const & morceau : discographie)
    {
        if (morceau.compositeur.nom != artiste_precedent.nom)
        {
            std::cout << "--> " << morceau.compositeur << std::endl;
        }

        if (morceau.album.nom != album_precedent.nom)
        {
            std::cout << "\t/--> " << morceau.album << std::endl;
        }

        std::cout << "\t\t/--> " << morceau.nom << std::endl;

        artiste_precedent = morceau.compositeur;
        album_precedent = morceau.album;
    }
}

// L'ensemble des possibilités d'affichage de la discographie.
enum class Affichage { Artiste, Album, Morceau };

void affichage(Discographie& discographie, Affichage type_affichage)
{
    if (type_affichage == Affichage::Album)
    {
        tri_album(discographie);
        affichage_album(discographie);
    }
    else if (type_affichage == Affichage::Artiste)
    {
        tri_artiste(discographie);
        affichage_artiste(discographie);
    }
    else if (type_affichage == Affichage::Morceau)
    {
        tri_morceau(discographie);
        affichage_morceau(discographie);
    }
    else
    {
        // Par exemple si on met à jour l'énumération mais qu'on oublie d'ajouter la fonction correspondante.
        throw std::runtime_error("Commande d'affichage inconnue.");
    }
}

void enregistrement(Discographie const & discographie, std::string const & nom_fichier)
{
    std::ofstream fichier { nom_fichier };
    if (!fichier)
    {
        throw std::runtime_error("Impossible d'ouvrir le fichier en écriture.");
    }

    for (Morceau const & morceau : discographie)
    {
        fichier << morceau << std::endl;
    }
}

void chargement(Discographie & discographie, std::string const & nom_fichier)
{
    std::ifstream fichier { nom_fichier };
    if (!fichier)
    {
        throw std::runtime_error("Impossible d'ouvrir le fichier en lecture.");
    }

    std::string ligne {};
    while (std::getline(fichier, ligne))
    {
        Morceau morceau {};

        std::istringstream flux { ligne };
        flux >> morceau;

        discographie.push_back(morceau);
    }
}
Commandes
/systeme_commandes.hpp
#ifndef SYSTEME_COMMANDES_HPP_INCLUDED
#define SYSTEME_COMMANDES_HPP_INCLUDED

#include "donnees_disco.hpp"

#include <string>
#include <tuple>

std::string recuperer_commande();

enum class Commande { Afficher, Ajouter, Enregistrer, Charger, Quitter };

std::tuple<Commande, std::string> analyser_commande(std::string const & commande_texte);
bool executer_commande(Discographie & discographie, Commande commande, std::string const & instructions);

#endif
/systeme_commandes.cpp
#include "discographie.hpp"
#include "systeme_commandes.hpp"
#include "utils.hpp"

#include <sstream>
#include <stdexcept>
#include <string>

std::string recuperer_commande()
{
    std::cout << "> ";

    std::string commande {};
    std::getline(std::cin, commande);
    return commande;
}

std::tuple<Commande, std::string> analyser_commande(std::string const & commande_texte)
{
    std::istringstream flux { commande_texte };

    std::string premier_mot {};
    flux >> premier_mot;
    premier_mot = traitement_chaine(premier_mot);

    std::string instructions {};
    std::getline(flux, instructions);
    instructions = traitement_chaine(instructions);

    if (premier_mot == "afficher")
    {
        return { Commande::Afficher, instructions };
    }
    else if (premier_mot == "ajouter")
    {
        return { Commande::Ajouter, instructions };
    }
    else if (premier_mot == "enregistrer")
    {
        return{ Commande::Enregistrer, instructions };
    }
    else if (premier_mot == "charger")
    {
        return { Commande::Charger, instructions };
    }
    else if (premier_mot == "quitter")
    {
        return { Commande::Quitter, std::string {} };
    }
    else
    {
        throw std::runtime_error("Commande invalide.");
    }
}

bool executer_commande(Discographie & discographie, Commande commande, std::string const & instructions)
{
    if (commande == Commande::Afficher)
    {
        if (instructions == "artistes")
        {
            affichage(discographie, Affichage::Artiste);
        }
        else if (instructions == "albums")
        {
            affichage(discographie, Affichage::Album);
        }
        else if (instructions == "morceaux")
        {
            affichage(discographie, Affichage::Morceau);
        }
        else
        {
            throw std::runtime_error("Commande d'affichage inconnue.");
        }
    }
    else if (commande == Commande::Ajouter)
    {
        std::istringstream flux { instructions };
        Morceau morceau {};
        flux >> morceau;
        discographie.push_back(morceau);
    }
    else if (commande == Commande::Charger)
    {
        chargement(discographie, instructions);
    }
    else if (commande == Commande::Enregistrer)
    {
        enregistrement(discographie, instructions);
    }
    else if (commande == Commande::Quitter)
    {
        return false;
    }
 
    return true;
}

Notre fichier main.cpp devient alors très simple.

#include "donnees_disco_tests.hpp"
#include "discographie.hpp"
#include "systeme_commandes.hpp"
#include "utils.hpp"

#include <stdexcept>

int main()
{
    test_creation_morceau_entree_complete();
    test_creation_morceau_entree_espaces_partout();
    test_creation_morceau_entree_chanson_artiste();
    test_creation_morceau_entree_chanson_uniquement();
    test_creation_morceau_entree_vide();

    Artiste const dave_brubeck { "Dave Brubeck " };
    Artiste const secret_garden { "Secret Garden" };
    Artiste const indochine { "Indochine" };

    Album const time_out { "Time Out" };
    Album const songs_from_a_secret_garden { "Songs from a Secret Garden" };
    Album const l_aventurier { "L'aventurier" };
    Album const paradize { "Paradize" };

    Morceau const take_five { "Take Five", dave_brubeck, time_out };
    Morceau const blue_rondo_a_la_turk { "Blue Rondo a la Turk", dave_brubeck, time_out };
    Morceau const nocturne { "Nocturne", secret_garden, songs_from_a_secret_garden };
    Morceau const aventurier { "L'aventurier", indochine, l_aventurier };
    Morceau const j_ai_demande_a_la_lune { "J'ai demandé à la lune", indochine, paradize };

    Discographie discographie { take_five, blue_rondo_a_la_turk, nocturne, aventurier, j_ai_demande_a_la_lune };

    bool continuer { true };
    do
    {
        try
        {
            std::string entree { recuperer_commande() };
            auto[commande, instructions] = analyser_commande(entree);
            instructions = traitement_chaine(instructions);
            continuer = executer_commande(discographie, commande, instructions);
        }
        catch (std::runtime_error const & exception)
        {
            std::cout << "Erreur : " << exception.what() << std::endl;
        }

    } while (continuer);

    return 0;
}

Et voilà ! On a un code organisé en plusieurs fichiers, prêt à évoluer !

Les avantages du découpage en fichiers

Le découpage en fichiers permet de mieux organiser le code, certes. Mais cela ne s’arrête pas là ; il y a d’autres avantages qu’on va voir maintenant.

Une structure de projet plus visible

Imaginez un projet encore plus conséquent que le T.P précédent. Tout est dans le même fichier. Compliquer après d’y voir un ordre quelconque. Si, par contre, le projet est découpé intelligemment en plusieurs fichiers, vous pourrez, d’un coup d’œil, comprendre la structure du projet et mieux orienter vos recherches.

Le projet Zeste de Savoir, par exemple, est découpé en plein de fichiers, qui séparent les tests, l’affichage, la manipulation de la base de données, etc. Un développeur ayant des connaissances dans les technologies utilisées sait donc, d’un rapide regard, où il doit orienter ses recherches.

Une meilleure abstraction

L’avantage de séparer l’implémentation des prototypes et des définitions de type, notamment avec les fichiers d’en-tête, c’est que l’utilisateur ne dépend plus de détails techniques mais de fonctions générales. Si besoin est, on peut changer du tout au tout la façon d’écrire nos fonctions, pour les rendre plus sûres ou plus rapides et l’utilisateur n’y verra rien.

Vous, apprentis développeurs C++, dépendez des prototypes et abstractions de la bibliothèque standard. Par contre, en coulisse, l’implémentation n’est pas la même selon que vous utilisez Visual Studio, GCC ou Clang. Mais ça n’est pas un problème. Nous dépendons d’abstractions, non de détails. C’est un des grands principes d’un programme bien conçu.

De plus, puisque les détails techniques sont cachés dans les fichiers sources, les fichiers d’en-tête sont bien plus lisibles et n’exposent que les fonctions et données publiques qui intéressent leur utilisateur.

Une meilleure modularité

Séparer en plusieurs fichiers, c’est aussi autoriser des portions de codes à être réutilisées dans d’autres endroits. Vous le savez très bien, puisque vous faites de nombreux #include d’en-têtes de la bibliothèque standard. Il serait plus difficile et bien plus lourd d’utiliser std::string ou std::vector s’il n’y avait pas les fichiers d’en-tête correspondants, n’est-ce pas ?

Rien que dans le T.P précédent, le fait d’avoir séparé la fonction de traitement des chaînes de caractères, pour la mettre dans un fichier à part, nous autorise à la réutiliser ailleurs que ce qu’on avait originellement prévu.

Le cas des templates

Il me reste un cas particulier à aborder, celui des templates. Dans le chapitre qui leur est consacré, nous avions dit que le compilateur va instancier une fonction template chaque fois qu’elle est appelée avec un type réel différent. Mais pour l’instancier, le compilateur a besoin de son implémentation complète. Du coup, un code comme celui-ci ne peut pas fonctionner.

conteneur.hpp
#ifndef CONTENEUR_HPP
#define CONTENEUR_HPP

#include <iostream>

template <typename Collection>
void afficher(Collection const & iterable);

#endif
conteneur.cpp
#include "conteneur.hpp"

template <typename Collection>
void afficher(Collection const & iterable)
{
    for (auto const & e : iterable)
    {
        std::cout << e << std::endl;
    }
}
main.cpp
#include <iostream>
#include <vector>
#include "conteneur.hpp"

int main()
{
    std::vector<int> const tableau { 1, 2, 3, 4 };
    afficher(tableau);
    return 0;
}
Visual Studio : Erreur  LNK2019 symbole externe non résolu "void __cdecl afficher<class std::vector<int,class std::allocator<int> > >(class std::vector<int,class std::allocator<int> > const &)" (??$afficher@V?$vector@HV?$allocator@H@std@@@std@@@@YAXABV?$vector@HV?$allocator@H@std@@@std@@@Z) référencé dans la fonction _main
-----------------------------------------------
GCC : /usr/bin/ld: /tmp/cc7jhqie.o: in function `main':
main.cpp:(.text+0x6e): undefined reference to `void afficher<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > const&)'
collect2: error: ld a retourné le statut de sortie 1
-----------------------------------------------
Clang : /usr/bin/ld: /tmp/main-27208d.o: in function `main':
main.cpp:(.text+0x80): undefined reference to `void afficher<std::vector<int, std::allocator<int> > >(std::vector<int, std::allocator<int> > const&)'
clang-7: error: linker command failed with exit code 1 (use -v to see invocation)

Le compilateur va en effet se plaindre (avec une erreur cryptique, soit dit en passant), car il essaye d’instancier le template afficher mais ne trouve nulle part son implémentation. Afin de compiler, il faut que toutes nos fonctions templates soient déclarées et implémentées directement dans les fichiers d’en-tête. C’est contraire à ce que nous venons d’apprendre dans ce chapitre, mais c’est pourtant ce qu’il faut faire.

conteneur.hpp
#ifndef CONTENEUR_HPP
#define CONTENEUR_HPP

#include <iostream>

template <typename Collection>
void afficher(Collection const & iterable)
{
    for (auto const & e : iterable)
    {
        std::cout << e << std::endl;
    }
}

#endif
1
2
3
4
Lecteur attentif

Il n’y a vraiment pas d’autres solutions ? Parce que tu as dis toi-même que c’était bien que les détails techniques restent cachés dans les fichiers sources.

Eh bien figurez-vous que si ! En fait, il y a une astuce toute simple, qui consiste à déclarer les fonctions dans le fichier d’en-tête, mais à les implémenter dans un fichier séparé (souvent avec l’extension .tpp), fichier qui sera inclut à la fin du fichier d’en-tête. Ainsi, on cache les détails techniques dans un autre fichier tout en laissant la compilation se faire.

conteneur_impl.tpp
// Implémentation des fonctions templates.

template <typename Collection>
void afficher(Collection const & iterable)
{
    for (auto const & e : iterable)
    {
        std::cout << e << std::endl;
    }
}
conteneur.hpp
#ifndef CONTENEUR_HPP
#define CONTENEUR_HPP

#include <iostream>

template <typename Collection>
void afficher(Collection const & iterable);

// A la fin, on rajoute le fichier d'implémentation.
#include "conteneur_impl.tpp"

#endif
main.cpp
#include <iostream>
#include <vector>

// Le fichier d'implémentation est lui aussi inclut, du coup.
#include "conteneur.hpp"

int main()
{
    std::vector<int> const tableau { 1, 2, 3, 4 };
    afficher(tableau);
    return 0;
}
1
2
3
4

En résumé

  • Découper en fichiers permet de séparer les définitions des implémentations.
  • Les fichiers d’en-tête contiennent les définitions et portent, dans ce cours, l’extension .hpp.
  • Les fichiers source contiennent les implémentations et les détails techniques. Ils terminent par l’extension .cpp.
  • Lors de la création de fichiers d’en-tête, il faut bien penser à les sécuriser avec les directives de préprocesseur abordées.
  • Découper son projet en fichiers permet une meilleure modularité, une meilleure réutilisation, une structure de projet plus visible.
  • Dans le cas des templates, il faut que l’implémentation soit écrite directement dans le fichier d’en-tête, soit dans un fichier séparé qui lui-même sera inclut dans le fichier d’en-tête concerné.