web-dev-qa-db-fra.com

"Référence non définie à" constructeur de classe de modèle

Je ne sais pas pourquoi cela se produit, car je pense que tout est correctement déclaré et défini.

J'ai le programme suivant, conçu avec des modèles. C'est une simple implémentation d'une file d'attente, avec les fonctions membres "add", "substract" et "print".

J'ai défini le noeud de la file d'attente dans le fichier "nodo_colaypila.h":

#ifndef NODO_COLAYPILA_H
#define NODO_COLAYPILA_H

#include <iostream>

template <class T> class cola;

template <class T> class nodo_colaypila
{
        T elem;
        nodo_colaypila<T>* sig;
        friend class cola<T>;
    public:
        nodo_colaypila(T, nodo_colaypila<T>*);

};

Ensuite, l'implémentation dans "nodo_colaypila.cpp"

#include "nodo_colaypila.h"
#include <iostream>

template <class T> nodo_colaypila<T>::nodo_colaypila(T a, nodo_colaypila<T>* siguiente = NULL)
{
    elem = a;
    sig = siguiente;//ctor
}

Ensuite, la définition et la déclaration de la classe de modèle de file d’attente et de ses fonctions:

"cola.h":

#ifndef COLA_H
#define COLA_H

#include "nodo_colaypila.h"

template <class T> class cola
{
        nodo_colaypila<T>* ult, pri;
    public:
        cola<T>();
        void anade(T&);
        T saca();
        void print() const;
        virtual ~cola();

};


#endif // COLA_H

"cola.cpp":

#include "cola.h"
#include "nodo_colaypila.h"

#include <iostream>

using namespace std;

template <class T> cola<T>::cola()
{
    pri = NULL;
    ult = NULL;//ctor
}

template <class T> void cola<T>::anade(T& valor)
{
    nodo_colaypila <T> * nuevo;

    if (ult)
    {
        nuevo = new nodo_colaypila<T> (valor);
        ult->sig = nuevo;
        ult = nuevo;
    }
    if (!pri)
    {
        pri = nuevo;
    }
}

template <class T> T cola<T>::saca()
{
    nodo_colaypila <T> * aux;
    T valor;

    aux = pri;
    if (!aux)
    {
        return 0;
    }
    pri = aux->sig;
    valor = aux->elem;
    delete aux;
    if(!pri)
    {
        ult = NULL;
    }
    return valor;
}

template <class T> cola<T>::~cola()
{
    while(pri)
    {
        saca();
    }//dtor
}

template <class T> void cola<T>::print() const
{
    nodo_colaypila <T> * aux;
    aux = pri;
    while(aux)
    {
        cout << aux->elem << endl;
        aux = aux->sig;
    }
}

Ensuite, j'ai un programme pour tester ces fonctions comme suit:

"main.cpp"

#include <iostream>
#include "cola.h"
#include "nodo_colaypila.h"

using namespace std;

int main()
{
    float a, b, c;
    string d, e, f;
    cola<float> flo;
    cola<string> str;

    a = 3.14;
    b = 2.71;
    c = 6.02;
    flo.anade(a);
    flo.anade(b);
    flo.anade(c);
    flo.print();
    cout << endl;

    d = "John";
    e = "Mark";
    f = "Matthew";
    str.anade(d);
    str.anade(e);
    str.anade(f);
    cout << endl;

    c = flo.saca();
    cout << "First In First Out Float: " << c << endl;
    cout << endl;

    f = str.saca();
    cout << "First In First Out String: " << f << endl;
    cout << endl;

    flo.print();
    cout << endl;
    str.print();

    cout << "Hello world!" << endl;
    return 0;
}

Mais lorsque je construis, le compilateur génère des erreurs dans chaque instance de la classe de modèle:

référence indéfinie à `cola (float) :: cola () '... (c'est en fait cola' <'float'> ':: cola (), mais cela ne me permet pas de l'utiliser comme ça.)

Etc. Au total, 17 avertissements, en comptant ceux des fonctions membres appelées dans le programme.

Pourquoi est-ce? Ces fonctions et constructeurs ont été définis. Je pensais que le compilateur pourrait remplacer le "T" dans le modèle par "float", "string" ou autre; c'était l'avantage d'utiliser des modèles.

J'ai lu quelque part ici que je devrais mettre la déclaration de chaque fonction dans le fichier d'en-tête pour une raison quelconque. Est-ce correct? Et si oui, pourquoi?

Merci d'avance.

126
Heathcliff

C’est une question courante dans la programmation C++. Il y a deux réponses valables à cela. Les deux réponses présentent des avantages et des inconvénients et votre choix dépendra du contexte. La réponse courante est de mettre toute l'implémentation dans le fichier d'en-tête, mais une autre approche conviendra dans certains cas. Le choix t'appartient.

Le code dans un modèle est simplement un "motif" connu du compilateur. Le compilateur ne compilera pas les constructeurs cola<float>::cola(...) et cola<string>::cola(...) jusqu'à ce qu'il soit obligé de le faire. Et nous devons nous assurer que cette compilation a lieu pour les constructeurs au moins une fois dans tout le processus de compilation, sinon nous obtiendrons l'erreur "référence non définie". (Ceci s’applique aux autres méthodes de cola<T> aussi.)

Comprendre le problème

Le problème est dû au fait que main.cpp et cola.cpp sera compilé séparément en premier. Dans main.cpp, le compilateur va implicitement instancier les classes de template cola<float> et cola<string> parce que ces instanciations particulières sont utilisées dans main.cpp. La mauvaise nouvelle est que les implémentations de ces fonctions membres ne sont pas en main.cpp, ni dans aucun fichier d’en-tête inclus dans main.cpp, et par conséquent le compilateur ne peut pas inclure de versions complètes de ces fonctions dans main.o. Lors de la compilation cola.cpp, le compilateur ne compilera pas ces instanciations non plus, car il n’existe aucune instanciation implicite ou explicite de cola<float> ou cola<string>. Rappelez-vous, lors de la compilation cola.cpp, le compilateur n'a aucune idée des instanciations nécessaires; et on ne peut pas s’attendre à ce qu’il soit compilé pour le type chaque afin de s’assurer que ce problème ne se produise jamais! (cola<int>, cola<char>, cola<ostream>, cola< cola<int> > ... etc ...)

Les deux réponses sont:

  • Dites au compilateur, à la fin de cola.cpp, quelles classes de gabarit seront nécessaires, ce qui l’obligera à compiler cola<float> et cola<string>.
  • Mettez l’implémentation des fonctions membres dans un fichier d’en-tête qui sera inclus chaque fois toute autre "unité de traduction" (telle que main.cpp) utilise la classe de modèle.

Réponse 1: Instanciez explicitement le modèle et ses définitions de membres

Au fin de cola.cpp, vous devez ajouter des lignes instanciant explicitement tous les modèles pertinents, tels que

template class cola<float>;
template class cola<string>;

et vous ajoutez les deux lignes suivantes à la fin de nodo_colaypila.cpp:

template class nodo_colaypila<float>;
template class nodo_colaypila<std :: string>;

Cela garantira que, lorsque le compilateur compile, cola.cpp qu'il compilera explicitement tout le code pour le cola<float> et cola<string> Des classes. De même, nodo_colaypila.cpp contient les implémentations du nodo_colaypila<...> Des classes.

Dans cette approche, vous devez vous assurer que toute la mise en œuvre est placée dans un .cpp fichier (c'est-à-dire une unité de traduction) et que l'instantation explicite est placée après la définition de toutes les fonctions (c'est-à-dire à la fin du fichier).

Réponse 2: copiez le code dans le fichier d'en-tête approprié

La réponse courante est de déplacer tout le code des fichiers d'implémentation cola.cpp et nodo_colaypila.cpp en cola.h et nodo_colaypila.h. À long terme, cela est plus flexible car cela signifie que vous pouvez utiliser des instanciations supplémentaires (par exemple, cola<char>) sans plus de travail. Mais cela pourrait signifier que les mêmes fonctions sont compilées plusieurs fois, une fois dans chaque unité de traduction. Ce n'est pas un gros problème, car l'éditeur de liens ignorera correctement les implémentations en double. Mais cela pourrait ralentir un peu la compilation.

Sommaire

La réponse par défaut, utilisée par exemple par la STL et dans la plupart du code que chacun écrira, consiste à placer toutes les implémentations dans les fichiers d'en-tête. Mais dans un projet plus privé, vous aurez plus de connaissances et de contrôle sur les classes de modèles particulières qui seront instanciées. En fait, ce "bogue" peut être considéré comme une fonctionnalité, car il empêche les utilisateurs de votre code d'utiliser accidentellement des instanciations que vous n'avez pas testées ou prévues ("Je sais que cela fonctionne pour cola<float> et cola<string>, si vous voulez utiliser autre chose, dites-le-moi d'abord et vous pourrez vérifier que tout fonctionne bien avant de l'activer. ").

Enfin, il y a trois autres fautes de frappe mineures dans le code de votre question:

  • Il vous manque un #endif à la fin de nodo_colaypila.h
  • dans cola.h nodo_colaypila<T>* ult, pri; devrait être nodo_colaypila<T> *ult, *pri; - les deux sont des pointeurs.
  • nodo_colaypila.cpp: le paramètre par défaut devrait être dans le fichier d'en-tête nodo_colaypila.h, pas dans ce fichier d'implémentation.
334
Aaron McDaid

Vous devrez définir les fonctions à l'intérieur de votre fichier d'en-tête.
Vous ne pouvez pas séparer la définition des fonctions de modèle dans le fichier source et les déclarations dans le fichier d’en-tête.

Lorsqu'un modèle est utilisé de manière à déclencher son intstantation, un compilateur doit voir cette définition de modèle particulière. C'est la raison pour laquelle les modèles sont souvent définis dans le fichier d'en-tête dans lequel ils sont déclarés.

Référence:
norme C++ 03, § 14.7.2.4:

La définition d'un modèle de fonction non exporté, d'un modèle de fonction de membre non exporté, d'une fonction de membre non exportée ou d'un membre de données statique d'un modèle de classe doit figurer dans chaque traduction unité dans laquelle il est explicitement instancié.

EDIT:
Pour clarifier la discussion sur les commentaires:
Techniquement, il existe trois façons de contourner ce problème de liaison:

  • Pour déplacer la définition dans le fichier .h
  • Ajouter des instanciations explicites dans le .cpp fichier.
  • #include le .cpp fichier définissant le modèle à la .cpp fichier en utilisant le modèle.

Chacun d’eux a ses avantages et ses inconvénients,

Déplacer les définitions dans les fichiers d'en-tête peut augmenter la taille du code (les compilateurs modernes peuvent l'éviter) mais augmentera le temps de compilation à coup sûr.

L'utilisation de l'approche d'instanciation explicite revient à une approche de type macro classique. Un autre inconvénient est qu'il est nécessaire de savoir quels types de gabarit sont requis par le programme. Pour un programme simple, c'est facile, mais pour un programme compliqué, cela devient difficile à déterminer à l'avance.

Tandis que l’inclusion de fichiers cpp est source de confusion en même temps, partage les problèmes des deux approches susmentionnées.

Je trouve que la première méthode est la plus facile à suivre et à mettre en œuvre et, partant, à l’utiliser.

11
Alok Save

Ce lien explique où vous vous trompez:

[35.12] Pourquoi ne puis-je pas séparer la définition de ma classe de modèles de sa déclaration et la placer dans un fichier .cpp?

Placez la définition de vos constructeurs, méthodes de destruction et tout le reste dans votre fichier d'en-tête, et cela corrigera le problème.

Ceci offre une autre solution:

Comment puis-je éviter les erreurs de l'éditeur de liens avec mes fonctions de modèle?

Toutefois, cela nécessite que vous anticipiez la manière dont votre modèle sera utilisé et qu’il s’agit d’une solution générale contre-intuitive. Cela résout le problème, mais vous développez un modèle à utiliser par un mécanisme interne et vous voulez contrôler la manière dont il est utilisé.

4
Liam M