Problème de classe template

Résolu/Fermé
Mourad2009B Messages postés 100 Date d'inscription lundi 23 août 2010 Statut Membre Dernière intervention 7 juillet 2023 - Modifié le 16 mars 2022 à 12:07
Mourad2009B Messages postés 100 Date d'inscription lundi 23 août 2010 Statut Membre Dernière intervention 7 juillet 2023 - 16 mars 2022 à 17:06
Bonjour à tous,

J'ai une classe template, dont voici le .h :
#define POINT_H
#include <iostream>
#include <windows.h>

template <class T> class point
{

private:
    T x, y;
 public:
    point(T abs=0, T ord=0)
    {
        x = abs, y = ord;
    }
    void  Affiche() const;
};

#endif // POINT_H


Et voici le .cpp

#include "point.h"

template <class T> void point<T>::Affiche() const
{
    unsigned int temp = GetConsoleOutputCP();
    SetConsoleOutputCP(CP_UTF8);
    printf("coordonnées : x = %d,    y = %d", x, y);
    SetConsoleOutputCP(temp);
}


Quand je fais de la fonction
Affiche
une fonction
inline
, elle est reconnue sans problème, mais quand je sépare la déclaration et la définition dans deux fichiers diffèrent comme actuellement, il génère l’erreur suivante :
D:\Fichiers_applications\C++\Projets_QtCreator\Test\Revis_gnrle\Patrons_de_classes\build-Patrons_de_classes-Desktop_Qt_5_15_2_MinGW_64_bit-Debug\..\Patrons_de_classes\main.cpp:9: erreur : undefined reference to `point<int>::Affiche() const'
debug/main.o: In function `main':
D:\Fichiers_applications\C++\Projets_QtCreator\Test\Revis_gnrle\Patrons_de_classes\build-Patrons_de_classes-Desktop_Qt_5_15_2_MinGW_64_bit-Debug/../Patrons_de_classes/main.cpp:9: undefined reference to `point<int>::Affiche() const'


Pourtant techniquement ça doit compiler ?
Merci d’avance pour votre aide

3 réponses

Dalfab Messages postés 706 Date d'inscription dimanche 7 février 2016 Statut Membre Dernière intervention 2 novembre 2023 101
15 mars 2022 à 21:15
Bonjour,

Une fonction ni
inline
ni
template
doit normalement être définie dans un fichiers source. Si dans un header utilisé dans plusieurs unités de compilation on a une erreur de redéfinition.

Une fonction
inline
peut être définie dans un fichier d'entête pour pouvoir l'utiliser dans plusieurs unités de compilation, on n'aura pas de problème de redéfinition.

Si la fonction est
template
, la définition doit être visible de l'endroit où va utiliser une spécialisation du
template
. Donc une template fonction doit normalement être définie dans un entête. A noter que pour les template fonctions on n'a pas besoin d'ajouter le mot
inline
, il est sous-entendu.
On peut tout de même définir une template fonction dans un fichier source, mais il faut alors explicitement indiquer les spécialisations que l'on veut instancier, indiquer qu'on les exporte vers les autres unités de compilation, et aussi indiquer explicitement leur importation. C'est un cas rare et complexe, donc la plupart du temps on doit les définir dans l'entête.
0
mamiemando Messages postés 33081 Date d'inscription jeudi 12 mai 2005 Statut Modérateur Dernière intervention 27 avril 2024 7 749
16 mars 2022 à 12:58
Bonjour,

Quelques précisions qui complète la réponse de Dalfab :

Quelques rappels préalables sur les headers et le linkage

Tout ce qui est dans cette section s'applique au C et au C++.

Lorsque le compilateur traite un fichier source (
.cpp
), chacune des fonctions (non statiques) de ce fichier est "référencée" dans le
.o
engendré à la compilation. C'est ce référencement qui va permettre au linker de retrouver le code binaire des fonctions au moment de construire le binaire final (l'exécutable ou la librairie que tu es en train de concevoir). Si un même nom (appelé symbole) est utilisé à plusieurs endroits (typiquement, la fonction
f
est référencée dans deux
.o
différent), le linker (c'est-à-dire la partie du compilateur qui "recolle" les modules entre eux pour former le binaire final) ne peut pas savoir de quelle fonction
f
tu parles et plantera en te disant qu'il y a une définition multiple.

Supposons que cette fonction
f
soit commune à deux modules (disons
a.cpp
et
b.cpp
), on ne peut pas se permettre de la copier coller dans ces deux fichiers, sans quoi on aura une erreur de linkage. Si on implémente la fonction
f
dans
f.cpp
et qu'on inclue ce fichier dans
a.cpp
et
b.cpp
, alors on a toujours le même problème, puisque faire un
#include
revient à copier coller le contenu du fichier. On ne peut donc se permettre que d'inclure sa déclaration. Et c'est typiquement ce qu'on va mettre dans un fichier header (disons
f.hpp
) que l'on incluera depuis
a.cpp
et
b.cpp
,, tandis que le code de
f
restera dans
f.cpp
.

Cependant, ça n'est pas suffisant.
a.o
et
b.o
sont compilés indépendamment et si on n'a pas veillé à mettre un verrou au niveau du
f.hpp
(e.g.
#ifndef F_HPP #define F_HPP ... #endif
) on aura f qui sera déclaré dans
a.o
et
b.o
. Une fois cette précaution prise, tout se passe bien. Peut importe dans quel ordre le compilateur traite
a.o
et
b.o
, au moment de traiter le premier des deux, il découvrira la déclaration de
f
et au moment de traiter le second il ne réincluera pas
f.hpp
tout en ayant en tête sa déclaration. Au moment du linkage, le compilateur contrôle que la fonction
f
que tu as déclaré est implémentée une et une seule fois (donc dans un seul fichier
.o
, ici
f.o
).

Ce que je viens d'expliquer s'étend à tout ce qui est global et nommé : fonctions, structures, unions, etc. En C++, s'ajoutent à cette liste les classes et méthodes. Ces noms sont généralement appelés "symboles" par le compilateur.

Que fait le mot clé
template


Le mot clé
template
étant spécifique au C++, ce qui est expliqué ici ne concerne pas le C.

Il faut commencer par comprendre ce qu'est un
template
et ce que fait un compilateur. Un compilateur détermine pour chaque variable son type et en déduit la taille qu'elle occupe en mémoire.
  • Supposons que tu veuilles utiliser la classe template
    std::vector<T>

parce que dans ton code tu veux utiliser des
std::vector<int>
, des
std::vector<double>
. Un
int
est plus petit qu'un
double
et on comprend qu'en mémoire, ces deux structures ne feront pas la même taille (et n'ont pas le même type). En C, il faudrait coder une classe
vector_int
et
vector_double
qui seraient identique aux types prêts. En C++, on peut éviter ça grâce au mot clé
template
. Mais ce dernier ne dispense pas le compilateur de devoir générer "en interne" une classe qui correspond à ce que j'ai appelé
vector_int
et
vector_double
.
  • Maintenant, la classe template
    std::vector<T>
    est définie dans la STL. Or la STL ne peut pas "deviner" les vecteurs que tu vas avoir envie de construire dans ton programme. Il n'est donc pas possible de les compiler par avance.
  • Il faut donc que ton programme sache quel est le code de
    std::vector<T>
    et donc pour ça il doit l'inclure. Or, comme tu le sais, en C et en C++, on ne doit inclure QUE des headers. Ainsi, pour être réutilisable dans d'autres fichiers, le code template doit nécessairement être écrit dans un header
    • La seule exception à cette règle, c'est si ta classe template n'est utilisée que dans un fichier source. Dans ce cas, tu pourrais te permettre de déclarer cette classe template dans ce fichier source, puisque la classe template n'est résolue nulle part ailleurs).
  • Au moment de compiler ton programme, le compilateur découvre toutes tes utilisations
    std::vector<T>
    et compiler ses déclinaisons, puis les utiliseras pour compiler ton programme.


Le mot clé
inline


Le mot clé
inline
étant spécifique au C++, ce qui est expliqué ici ne concerne pas le C.

Pour une fonction normale, le compilateur prévoit un appel et tout ce que cela comporte (recopie des paramètres dans la pile, exécution de la fonction, retour à la fonction appelante).

En C++, quand une fonction est
inline
, le compilateur substitue son appel par le code de la fonction elle même. Cela veut dire que si ta fonction inline est appelée a 100 endroits dans ton code, le binaire associé à ta fonction
inline
est copié à ces 100 endroits. Bien entendu, ce "copier coller" induit un binaire plus gros, mais il permet d'optimiser l'exécution car on évite de recopier des paramètres en pile. Cependant, ce coût fait qu'on réserve ce mot clé généralement aux fonctions/méthodes courtes.

Il a un autre effet. Comme la fonction
inline
est substituée, son nom n'est pas un symbole en tant que tel (voir la section qui fait les rappels préalables sur le linkage). Cela veut dire que contrairement à une fonction ordinaire, une fonction
inline
peut être implémentée dans un header sans engendrer de définitions multiples.

En d'autres termes, si tu veux implémenter une fonction (non template) dans un header, celle-ci doit être
inline
, sinon tu cours le risque d'avoir une définition multiple.

Résumé
  • Une classe template est généralement codée dans un fichier header. On ne peut se permettre de l'implémenter dans un fichier source que si elle n'est utilisée que dans ce fichier source.
  • Les fonctions / méthodes / classes / structures / ... sont généralement déclarées dans un header, ce qui permet de les réutiliser dans d'autres fichiers. Les méthodes et fonctions sont implémentées dans le fichier source. Le header est verrouillé pour n'être inclu qu'une fois au moment de la compilation. Ces conventions permettent de garantir que chaque symbole est déclaré une et une seule fois et compilé une et une seule fois, ce qui évite les erreurs de linkage (définition ou déclarations multiples, symbole indéfinis, etc).
  • Le mot clé inline permet d'implémenter une fonction dans un header, car une telle fonction n'engendre pas de symbole au moment de compiler. Il est généralement réservé au fonction courte. Il n'a rien à voir avec la notion de
    template
    . Une classe template peut comporter (ou pas) des méthodes
    inline
    , c'est une considération indépendantes.


Bonne chance
0
Mourad2009B Messages postés 100 Date d'inscription lundi 23 août 2010 Statut Membre Dernière intervention 7 juillet 2023
Modifié le 16 mars 2022 à 19:16
Bonjour Daflab,

Merci pour tes éclaircissements, ça permet de gagner du temps, (beaucoup de temps)
C'est un exemple que j'ai trouvé dans un livre, mais maintenant je déclare tout dans le fichier header
#ifndef POINT_H
#define POINT_H
#include <iostream>
#include <windows.h>

template <class T> class point
{
public:
    point(T abs=0, T ord=0)
    {
        x = abs, y = ord;
    }
    
    void Affiche() const;
    
private:
    T x, y;
};


template<class T> void point<T>::Affiche() const
{
    unsigned int temp = GetConsoleOutputCP();
    SetConsoleOutputCP(CP_UTF8);
    std::cout << "coordonnées : x = " << x << ",    y = "  << y << std::endl;
    SetConsoleOutputCP(temp);
}

//Spécialisation pour les char
template<> void point<char>::Affiche() const
{
    std::cout << "coordonnées : x = " << (int)x << ",    y = "  << (int)y << std::endl;
}

#endif // POINT_H


Merci beaucoup.
0