IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

déclaration de template dans un header


Sujet :

C++

  1. #1
    Membre régulier
    Homme Profil pro
    Étudiant
    Inscrit en
    Mars 2020
    Messages
    95
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France, Ariège (Midi Pyrénées)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mars 2020
    Messages : 95
    Points : 76
    Points
    76
    Par défaut déclaration de template dans un header
    bonjour à tous, dans le cours de c++ moderne sur Zeste de savoir, la partie sur la programmation modulaire explique qu'il faut déclarer les templates comme ceci:

    conteneur_impl.tpp:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    // 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

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #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
    et le main.cpp:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #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;
    }

    est-ce que la déclaration du prototype du template dans conteneur.hpp n'est pas inutile?

  2. #2
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 615
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 615
    Points : 30 628
    Points
    30 628
    Par défaut
    Salut,

    Tu n'as, de toutes évidences, pas compris à quoi servent les déclarations (de fonctions) par oppositions aux implémentations. Je m'en vais donc te l'expliquer

    Le compilateur va traiter ton code exactement de la même manière que tu lirais un bon roman: du début à la fin.

    Tout comme toi, tu ne sais pas, quand tu lis la page dix de ton roman ce qui peut se passer à la page onze ou à la page douze (mais que tu sais, par contre, ce qui s'est passé pendant les neuf premières pages), le compilateur ne connait, quand il traite la ligne 10 de ton code, que ce qu'il a pu croiser pendant les 9 premières lignes, et il n'a absolument aucune idée de ce qui se passe à la ligne 11 ou à la ligne 12.

    Or, le compilateur n'acceptera que l'on utilise dans notre code que ce qu'il a déjà rencontré, ce dont il sait que "ca existe".

    Quant il n'y a qu'une seule fonction (et un seul fichier), il y a très peux de choses qui deviennent utiles ou nécessaires, si bien que tu pourrais tout aussi bien te simplifier la vie et placer l'implémentation de la fonction dans le fichier d'en-tête hpp (n'oublie pas de la déclarer inline, dans tous les cas ).

    Mais quand il y a plusieurs fonctions, les choses deviennent tout de suite beaucoup plus compliquées, car tu vas devoir choisir l'ordre dans lequel les fonctions seront implémentées:

    Tu ne peux, en effet, implémenter qu'une seule fonction à la fois, et donc, si tu as trois fonctions, foo(), bar() et truc(), tu peux décider de les implémenter dans l'ordre suivant:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void foo(){
        /* ... */
    }
    void bar(){
        /* ... */
    }
    void truc(){
        /* ... */
    }
    ou peut-être préféreras tu le faire dans l'ordre
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    void truc(){
        /* ... */
    }
    void foo(){
        /* ... */
    }
    void bar(){
        /* ... */
    }
    ou peut être te trouveras tu avec ... n'importe laquelle des six combinaisons possibles. Et le fait est que ton programme doit pouvoir fonctionner quelle que soit la combinaison choisie!

    En fait, on se fout pas mal de l'ordre dans lequel tu vas implémenter tes fonctions, d'autant plus que, si cela se trouve, tu auras commencé par créer la fonction truc(), puis tu te sera rendu compte que tu as besoin de la fonction bar() (et tu l'auras implémentée) avant de terminer par la fonction foo(), n'est-ce pas

    Là où les choses vont devenir plus compliquées, c'est quand tu vas devoir faire appel à une ou plusieurs fonctions depuis une fonction bien précise; quand foo() voudra faire appel à bar(), que bar() voudra faire appel à truc() et que truc voudras faire appel à foo() (par exemple) sous une forme qui pourrait ressembler à quelque chose comme

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void foo(){
        if(condition)
            bar();
    }
    void bar(){
        if(condition)
           truc();
    }
    void truc(){
        if(condition)
            foo();
    }
    Ca a l'air pas mal comme ordre pour implémenter nos trois fonctions, n'est-ce pas

    Sauf que, quand le compilateur va traiter ton fichier, il va t'engueuler
    • à la ligne 3, parce que, en ayant lu les lignes 1 et 2 il ne connait pas la fonction bar()
    • à la ligne 7, parce que, ayant lu les six premières lignes, il ne connait pas la fonction truc

    C'est pas grave! essayons de mettre les fonctions dans un ordre qui permettra au compilateur d'être content:
    foo() doit donc être implémentée après bar() pour que le compilateur puisse connaitre la fonction bar() au moment où on y fait appel depuis la fonction foo(). De même, bar() doit être implémentée après truc() pour les mêmes raisons.

    Très bien, modifions l'ordre d'implémentation de nos fonctions, et comme foo() doit être implémentée après bar() qui doit être implémentée après truc, l'ordre "logique" nous fera essayer le code

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void truc(){
        if(condition)
            foo();
    }
    void bar(){
        if(condition)
           truc();
    }
    void foo(){
        if(condition)
            bar();
    }
    On réessaye de compiler et... le compilateur n'est toujours pas content Ben oui, quand il traite la ligne 3 (qui est une ligne de truc), il ne connait pas la fonction foo().

    Si bien que cette fonction truc() dont on a déterminé qu'elle devrait être implémentée en premier (pour que le compilateur puisse connaitre toutes les fonctions lors de leur appel) devrait en fait être implémentée après foo() (dont on a déterminé qu'elle devrait être implémentée en dernier), et ca, c'est pas possible en l'état.

    C'est à ce moment là que les déclarations de fonctions font leur apparition. Car, ce qui nous embête, c'est que le compilateur ne connait tout simplement pas une (au moins) une fonction au moment où on y fait appel; que le compilateur ne sait pas qu'il existe une fonction du nom utilisé ne prenant aucun paramètre et ne renvoyant aucune valeur (dans le cadre de notre exemple, bien sur).

    Hé bien, arrangeons nous pour que le compilateur sache que les différentes fonctions existent en fournissant une déclaration pour ces fonctions:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void foo();  // j'indique au compilateur que la fonction foo existe
    void bar();  // j'indique au compilateur que la fonction bar existe
    void truc();  // j'indique au compilateur que la fonction truc existe
     
    /* A partir d'ici, quelle que soit la fonction à laquelle je ferai appel,
     * le compilateur saura qu'elle existe, et que l'appel correspond
     * à la manière dont la fonction est sensée être utilisée
     */
    void foo(){
        if(condition)
            bar(); // OK: le compilateur sait que la fonction bar() existe
    }
    void bar(){
        if(condition)
           truc(); // OK: le compilateur sait que la fonction truc() existe
    }
    void truc(){
        if(condition)
            foo();  // OK: le compilateur sait que la fonction foo() existe
    }
    Dis toi bien qu'un fichier d'en-tête n'a qu'un seul intérêt: permettre au compilateur de savoir "qu'un certain nombre de choses" existent et qu'elles sont susceptibles d'être utilisées.

    Le fait que le compilateur sache que ces choses existent nous permet d'utiliser ces éléments (de faire appel au fonctions déclarées) dans "n'importe quel ordre".

    Nous pourrions parfaitement ne pas utiliser de fichier d'en-tête, et commencer systématiquement nos fichiers d'implémentation en ... déclarant l'ensemble des éléments utilisés à la main.

    Ca serait cool (mais augmenterait considérablement le risque d'erreur) pour un fichier ou deux, mais, ca deviendrait rapidement ingérable avec l'augmentation de la complexité de notre projet
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  3. #3
    Membre du Club
    Homme Profil pro
    .
    Inscrit en
    Octobre 2019
    Messages
    21
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Autre

    Informations professionnelles :
    Activité : .

    Informations forums :
    Inscription : Octobre 2019
    Messages : 21
    Points : 44
    Points
    44
    Par défaut
    Oui, "inutile" dans le sens que ça marcherait toujours si l'on supprimait. L'auteur veut pourtant exposer le moins de détail technique possible et pour ça il les met dans le fichier conteneur_impl.hpp. Par conséquent, conteneur.hpp ne contient que ce qui constitue l'interface, l'implementation reste cachée. Un changement d'implementation n'affecte pas du tout l'interface.

  4. #4
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 615
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 615
    Points : 30 628
    Points
    30 628
    Par défaut
    Le fait est que la déclaration de fonction poursuit un but: permettre au compilateur de savoir que la fonction existe même s'il n'en a pas encore croiser l'implémentation.

    En s'organisant un tout petit peu, il y a moyen, dans les cas les plus simples, de se passer de la déclaration d'une fonction pour la simple et bonne raison que toute implémentation vaut déclaration.

    Ainsi, nous pourrions avoir un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    template <typename Collection>
    void afficher(Collection const & iterable)
    {
        for (auto const & e : iterable)
        {
            std::cout << e << std::endl;
        }
    }
     
    int main(){
        std::vector<int> tabInt;
        /* ... */
        std::list<std::string> listStrings;
        /* ... */
        afficher(tabInt);
        afficher(listStrings);
    }
    qui fonctionnerait sans aucun problème, car la fonction afficher a déjà été implémentée (et est donc d'office déclarée) lorsque le compilateur rencontre les appels que l'on y fait.

    Par contre, si on inverse la fonction maint et la fonction afficher pour avoir un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     
    int main(){
        std::vector<int> tabInt;
        /* ... */
        std::list<std::string> listStrings;
        /* ... */
        afficher(tabInt);
        afficher(listStrings);
    }
     
    template <typename Collection>
    void afficher(Collection const & iterable)
    {
        for (auto const & e : iterable)
        {
            std::cout << e << std::endl;
        }
    }
    Ca ne fonctionnera plus. Pourquoi parce que le compilateur ne saura pas, lorsqu'il croise l'appel à la fonction afficher() en ligne 7 et en ligne 8, que cette fonction existe bel et bien et qu'elle peut être appelée avec un paramètre de type std::vector<int> ou avec un paramètre de type std::list<std::string>.

    Or, les développeurs sont des gens étranges, dans le sens où ils ont tous leurs petites manies personnelles.

    Par exemple, je connais des développeurs qui veulent systématiquement que la fonction main soit la première fonction définie dans le fichier main.cpp (ce qui prend d'ailleurs tout son sens quand on sait que, a priori, ce devrait aussi être la seule fonction à être implémentée dans ce fichier )

    Du coup, si l'on veut s'assurer que les deux codes fonctionneront quoi qu'il arrive, il faut fournir une déclaration de la fonction "quelque part" où l'on sera sur que nous n'essayerons jamais de l'appeler avant qu'elle soit déclarée.

    Or, s'il y a bien une chose sur laquelle tous les développeurs C++ sont d'accords (ca n'arrive pas souvent, ca vaut donc la peine d'être signalé ), c'est que tous les fichier d'en-tête doivent être inclus avant de faire quoi que ce soit d'autre.

    Et donc, si je déclare ma fonction dans un fichier d'en-tête, sous la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    template <typename Collection>
    void afficher(Collection const & iterable);
    je peux être sur que, si le développeur n'oublie pas d'inclure le fichier d'en-tête, il le fera avant d'essayer de fournir l'implémentation de la moindre fonction

    Au delà de ca, il y a -- bien sur -- le problème spécifique aux fonctions template, dont le code binaire exécutable ne sera généré qu'une fois que le compilateur sauras par quel type de donnée remplacer les différents paramètres template.

    Il faudra donc s'assurer que l'implémentation de la fonction template soit "accessible" à chaque fois que l'on fait appel à la fonction. Mais ca, ca reste du détail
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  5. #5
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 116
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 116
    Points : 32 968
    Points
    32 968
    Billets dans le blog
    4
    Par défaut
    Dans l'absolu oui elle est inutile.
    En pratique, il est préférable d'avoir un fichier header simple avec des prototypes lisibles qu'un fichier avec des implémentations de template (qui peuvent être assez tordues) qui serait très peu lisible et où il serait difficile de retrouver ce qui nous intéresse ou voir ce qui est disponible.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  6. #6
    Membre régulier
    Homme Profil pro
    Étudiant
    Inscrit en
    Mars 2020
    Messages
    95
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France, Ariège (Midi Pyrénées)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mars 2020
    Messages : 95
    Points : 76
    Points
    76
    Par défaut
    ok merci pour les réponses, ce que j'entendais par inutile c'est (dans ce cas bien précis) que lorsque le main va etre lu par le compileur ça va donner ceci:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <iostream>
    #include <vector>
     
    #include "conteneur.hpp"
     
    /*à ce moment le compileur lit:
     
    template <typename Collection>
    void afficher(Collection const & iterable);
     
    template <typename Collection>
    void afficher(Collection const & iterable)
    {
        for (auto const & e : iterable)
        {
            std::cout << e << std::endl;
        }
    }
    */
     
     
     
    int main()
    {
        std::vector<int> const tableau { 1, 2, 3, 4 };
        afficher(tableau);
        return 0;
    du coup j'entendais qu'elle est inutile dans ce cas la car le prototype et l'implémentation se suivent directement et sont appelés avant le main via la directive de préprocesseur

  7. #7
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 615
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 615
    Points : 30 628
    Points
    30 628
    Par défaut
    Et c'est ce que j'ai dit: avec une seule fonction, il n'y a pas grand chose qui soit réellement utile

    Cependant, c'est un très mauvais calcul que de se dire "bah, dans ce cas précis (une seule fonction), cela ne sert strictement à rien" (même si c'est parfaitement vrai), car on ne sait jamais ce que tu vas pouvoir rajouter par la suite, et que tes ajouts vont forcément changer la donne

    Etant entendu que ton code évoluera quoi qu'il arrive dans le temps, tu dois prendre -- dés maintenant -- l'habitude d'écrire ton code de manière à éviter au maximum les soucis lorsque tu voudras le faire évoluer.

    Si bien que, même s'il semble "inutile" de déclarer une "unique" fonction dans un fichier d'en-tête, tu as largement intérêt à le faire pour que... les évolutions futures soient simplifiées.

    En outre, tu dois te dire que, si tu sépare ton fichier d'en-tête en deux éléments (le fichier d'en-tête proprement dit et le fichier d'implémentation des fonctions template), c'est parce que ces deux parties poursuivent un objectif différent:

    Le fichier d'en-tête est avant tout fourni au développeur qui lira ton code pour lui permettre de se faire une idée de l'interface qu'il peut utiliser, des fonctionnalités sur lesquelles il peut compter, alors que le fichier d'implémentation est là pour lui cacher les détails sordides de l'implémentation.

    Car il faut bien te dire que si un développeur (ce peut être toi, dans trois mois, six mois ou un an) ne peut ouvrir qu'un seul fichier, ce sera le fichier d'en-tête, celui qui contient les déclarations (ou qui devrait normalement les contenir).

    C'est en effet le fichier d'en-tête qui lui permettra de répondre aux questions qu'il peut se poser et qui sont:
    • quelles sont les fonctions dont je dispose
    • quels paramètres (de quel type) dois-leur fournir, et dans quel ordre
    • quel type de donnée j'obtiens en retour (si tant est que j'en obtienne)

    La manière dont les fonctions fournissent le résultat, il n'en a rien à foutre, pour autant que le résultat soit correct, valide et cohérent

    Et, même toi qui est le développeur de la fonction, il faut bien te dire que tu n'as ce status de développeur de la fonction que ... le temps qu'il te faut pour en écrire le code.

    Une fois que tu as écrit le code d'une fonction et que tu l'as testée et validée, tu perds ton status de développeur de la fonction en question pour "redevenir" un simple utilisateur de celle-ci

    Au final la règle à suivre est: tu déclares tes fonctions (et tes types de données), même si tu as l'impression que cela ne sert à rien, parce que, en tant qu'utilisateur, tu n'en a rien à foutre de la manière dont la fonction fourni le résultat que l'on attend de sa part
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  8. #8
    Membre régulier
    Homme Profil pro
    Étudiant
    Inscrit en
    Mars 2020
    Messages
    95
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France, Ariège (Midi Pyrénées)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Mars 2020
    Messages : 95
    Points : 76
    Points
    76
    Par défaut
    ok donc effectivement autant prendre l'habitude de faire comme ça à chaque fois... merci bien!

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 1
    Dernier message: 25/04/2015, 10h32
  2. template dans header mais erreur au link
    Par ctxnop dans le forum Langage
    Réponses: 1
    Dernier message: 11/12/2013, 15h20
  3. Déclaration d'iterator dans template
    Par artefactman dans le forum C++
    Réponses: 2
    Dernier message: 10/06/2012, 11h36
  4. Réponses: 6
    Dernier message: 04/11/2011, 22h25
  5. Réponses: 4
    Dernier message: 23/08/2006, 15h31

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo