web-dev-qa-db-fra.com

La fonction de modèle C++ est compilée dans l'en-tête mais pas d'implémentation

J'essaie d'apprendre des modèles et j'ai rencontré cette erreur déconcertante. Je déclare certaines fonctions dans un fichier d'en-tête et je veux créer un fichier d'implémentation séparé dans lequel les fonctions seront définies. Voici le code qui appelle l'en-tête (dum.cpp):

#include <iostream>
#include <vector>
#include <string>
#include "dumper2.h"

int main() {
    std::vector<int> v;
    for (int i=0; i<10; i++) {
        v.Push_back(i);
    }
    test();
    std::string s = ", ";
    dumpVector(v,s);
}

Maintenant, voici un fichier d'en-tête de travail (dumper2.h):

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

void test();

template <class T> void dumpVector( std::vector<T> v,std::string sep);

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
    typename std::vector<T>::iterator vi;

    vi = v.begin();
    std::cout << *vi;
    vi++;
    for (;vi<v.end();vi++) {
        std::cout << sep << *vi ;
    }
    std::cout << "\n";
    return;
}

Avec implémentation (dumper2.cpp):

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

void test() {
    std::cout << "!olleh dlrow\n";
}

La chose étrange est que si je déplace le code qui définit dumpVector du fichier .h vers le fichier .cpp, j'obtiens l'erreur suivante.

g++ -c dumper2.cpp -Wall -Wno-deprecated
g++ dum.cpp -o dum dumper2.o -Wall -Wno-deprecated
/tmp/ccKD2e3G.o: In function `main':
dum.cpp:(.text+0xce): undefined reference to `void dumpVector<int>(std::vector<int, std::allocator<int> >, std::basic_string<char, std::char_traits<char>, std::allocator<char> >)'
collect2: ld returned 1 exit status
make: *** [dum] Error 1

Alors, pourquoi cela fonctionne-t-il dans un sens et non dans l'autre? Clairement, le compilateur peut trouver test(), alors pourquoi ne peut-il pas trouver dumpVector?

26
flies

Le problème que vous rencontrez est que le compilateur ne sait pas quelles versions de votre modèle instancier. Lorsque vous déplacez l'implémentation de votre fonction vers x.cpp, celle-ci se trouve dans une unité de traduction différente de main.cpp. Ce dernier ne peut pas être lié à une instanciation particulière, car elle n'existe pas dans ce contexte. Il s'agit d'un problème bien connu lié aux modèles C++. Il y a quelques solutions:

1) Il suffit de placer les définitions directement dans le fichier .h, comme vous le faisiez auparavant. Cela a des avantages et des inconvénients, y compris la résolution du problème (pro), peut-être rendre le code moins lisible et, sur certains compilateurs, plus difficile à déboguer (con) et peut-être augmenter le code gonflé (con).

2) Placez l'implémentation dans x.cpp et #include "x.cpp" à partir de x.h. Si cela semble funky et faux, gardez simplement à l'esprit que #include ne fait que lire le fichier spécifié et le compiler comme si ce fichier faisait partie de x.cppEn d'autres termes, cela correspond exactement à la solution n ° 1 ci-dessus, mais cela les garde dans des fichiers physiques séparés. En faisant ce genre de chose, il est essentiel que vous n'essayiez pas de compiler le fichier #included lui-même. Pour cette raison, je donne généralement à ces types de fichiers une extension hpp pour les distinguer des fichiers h et des fichiers cpp.

Fichier: dumper2.h

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

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);
#include "dumper2.hpp"

Fichier: dumper2.hpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;

}

3) Étant donné que le problème est qu'une instanciation particulière de dumpVector n'est pas connue de l'unité de traduction qui tente de l'utiliser, vous pouvez forcer une instanciation spécifique de celle-ci dans la même unité de traduction que celle où le modèle est défini. Simplement en ajoutant ceci: template void dumpVector<int>(std::vector<int> v, std::string sep); ... au fichier où le modèle est défini. Ce faisant, vous n'avez plus besoin de #include le fichier hpp à partir du fichier h:

Fichier: dumper2.h

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

void test();
template <class T> void dumpVector( std::vector<T> v,std::string sep);

Fichier: dumper2.cpp

template <class T> void dumpVector(std::vector<T> v, std::string sep) {
  typename std::vector<T>::iterator vi;

  vi = v.begin();
  std::cout << *vi;
  vi++;
  for (;vi<v.end();vi++) {
    std::cout << sep << *vi ;
  }
  std::cout << "\n";
  return;
}

template void dumpVector<int>(std::vector<int> v, std::string sep);

En passant, et en passant, votre fonction de modèle prend une vector par valeur . Vous pouvez ne pas vouloir faire cela, et le transmettre par référence ou par pointeur ou, mieux encore, transmettre des itérateurs à la place pour éviter de créer un temporaire et de copier le vecteur entier.

36
John Dibling

C’est ce que le mot clé export était supposé accomplir (c’est-à-dire que vous pouvez insérer le modèle export dans un fichier source au lieu d’un en-tête. Malheureusement, un seul compilateur (Comeau) a réellement implémenté export complètement.

Quant à la raison pour laquelle les autres compilateurs (y compris gcc) ne l’ont pas implémenté, la raison est assez simple: parce que export est extrêmement difficile à implémenter correctement. Code inside le modèle peut changer (presque) complètement de sens, en fonction du type sur lequel le modèle est instancié. Vous ne pouvez donc pas générer un fichier objet conventionnel du résultat de la compilation du modèle. Juste par exemple, x+y peut compiler en code natif tel que mov eax, x/add eax, y lorsqu’il est instancié sur une int, mais se compiler en appel de fonction s’il est instancié sur quelque chose comme std::string qui surcharge operator+.

Pour prendre en charge la compilation séparée de modèles, vous devez effectuer ce que l’on appelle une recherche de nom en deux phases (c’est-à-dire rechercher le nom à la fois dans le contexte du modèle et dans le contexte où le modèle est instancié). En règle générale, le compilateur compile le modèle dans une sorte de format de base de données pouvant contenir des instanciations du modèle sur une collection arbitraire de types. Vous ajoutez ensuite une étape entre la compilation et la liaison (bien que cela puisse être intégré à l’éditeur de liens, si vous le souhaitez) qui vérifie la base de données et, si elle ne contient pas de code pour le modèle instancié sur tous les types nécessaires, ré-invoque le compilateur. pour instancier sur les types nécessaires.

En raison des efforts extrêmes, du manque de mise en œuvre, etc., le comité a voté en faveur de la suppression de export de la prochaine version de la norme C++. Deux autres propositions (modules et concepts) assez différentes ont été faites, chacune fournissant au moins une partie de ce que export était censé faire, mais d'une manière qui soit (du moins espérée être) plus utile et raisonnable à mettre en œuvre.

9
Jerry Coffin

Les paramètres de modèle sont résolus au moment de la compilation.

Le compilateur trouve le fichier .h, trouve une définition correspondante pour dumpVector et la stocke. La compilation est terminée pour ce fichier .h. Ensuite, il continue à analyser les fichiers et à les compiler. Lorsqu'il lit l'implémentation dumpVector dans le fichier .cpp, il compile une unité totalement différente. Rien n'essaie d'instancier le modèle dans dumper2.cpp, le code de modèle est donc simplement ignoré. Le compilateur n'essaiera pas tous les types possibles pour le modèle, espérant qu'il y aura quelque chose d'utile plus tard pour l'éditeur de liens.

Ensuite, au moment du lien, aucune implémentation de dumpVector pour le type int n'a été compilée, donc l'éditeur de liens n'en trouvera aucun. D'où pourquoi vous voyez cette erreur.

Le mot clé export est conçu pour résoudre ce problème. Malheureusement, peu de compilateurs le prennent en charge. Alors gardez votre implémentation avec le même fichier que votre définition.

5
Julien Lebosquain

Une fonction de modèle n'est pas une vraie fonction. Le compilateur transforme une fonction de modèle en fonction réelle lorsqu'il rencontre une utilisation de cette fonction. Ainsi, l'intégralité de la déclaration de modèle doit être dans la portée, elle trouve l'appel à DumpVector, sinon elle ne peut pas générer la fonction réelle.
Étonnamment, beaucoup de livres d’introduction C++ s’y trompent.

2
Tim Kay

C’est exactement comme cela que fonctionnent les modèles en C++, vous devez mettre l’implémentation dans l’en-tête.

Lorsque vous déclarez/définissez une fonction de modèle, le compilateur ne peut pas, comme par magie, déterminer les types spécifiques avec lesquels vous souhaitez utiliser le modèle. Il ne peut donc pas générer de code à insérer dans un fichier .o comme avec une fonction normale. Au lieu de cela, il repose sur la génération d'une instanciation spécifique pour un type lorsqu'il voit l'utilisation de cette instanciation.

Ainsi, lorsque l’implémentation est dans le fichier .C, le compilateur dit en substance "hé, il n’existe aucun utilisateur de ce modèle, ne générez aucun code". Lorsque le modèle est dans l'en-tête, le compilateur peut voir l'utilisation dans main et générer le code de modèle approprié.

1
Mark B

La plupart des compilateurs ne vous autorisent pas à placer les définitions de fonction de modèle dans un fichier source séparé, même si cela est techniquement autorisé par la norme. 

Voir également:

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.12

http://www.parashift.com/c++-faq-lite/templates.html#faq-35.14

0
Cogwheel