Stockage des définitions de fonction de modèle C ++ dans un fichier .CPP

526

J'ai un code de modèle que je préférerais avoir stocké dans un fichier CPP au lieu d'être intégré dans l'en-tête. Je sais que cela peut être fait tant que vous savez quels types de modèles seront utilisés. Par exemple:

fichier .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

Fichier .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Notez les deux dernières lignes - la fonction de modèle foo :: do n'est utilisée qu'avec les chaînes ints et std ::, donc ces définitions signifient que l'application sera liée.

Ma question est - est-ce un hack méchant ou cela fonctionnera-t-il avec d'autres compilateurs / linkers? J'utilise uniquement ce code avec VS2008 pour le moment, mais je souhaiterai porter sur d'autres environnements.

Rob
la source
22
Je ne savais pas que c'était possible - une astuce intéressante! Cela aurait grandement aidé certaines tâches récentes à le savoir - cheers!
Xan
69
La chose qui me frappe est l'utilisation de docomme identifiant: p
Quentin
j'ai fait quelque chose de similaire avec gcc, mais j'effectue toujours des recherches
Nick
16
Ce n'est pas un "hack", c'est de la decleration vers l'avant. Cela a une place dans le standard de la langue; donc oui, il est autorisé dans tous les compilateurs conformes standard.
Ahmet Ipkin
1
Et si vous avez des dizaines de méthodes? Pouvez-vous simplement faire template class foo<int>;template class foo<std::string>;à la fin du fichier .cpp?
Ignorant

Réponses:

231

Le problème que vous décrivez peut être résolu en définissant le modèle dans l'en-tête ou via l'approche que vous décrivez ci-dessus.

Je recommande de lire les points suivants de la FAQ C ++ Lite :

Ils entrent dans beaucoup de détails sur ces problèmes de modèle (et d'autres).

Aaron N. Tubbs
la source
39
Juste pour compléter la réponse, le lien référencé répond positivement à la question, c'est-à-dire qu'il est possible de faire ce que Rob a suggéré et d'avoir le code pour être portable.
ivotron
161
Pouvez-vous simplement publier les parties pertinentes dans la réponse elle-même? Pourquoi un tel référencement est-il même autorisé sur SO. Je n'ai aucune idée de ce qu'il faut rechercher dans ce lien car il a été profondément modifié depuis.
Ident
124

Pour d'autres sur cette page qui se demandent quelle est la syntaxe correcte (comme je l'ai fait) pour la spécialisation explicite de modèles (ou au moins dans VS2008), c'est la suivante ...

Dans votre fichier .h ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

Et dans votre fichier .cpp

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
espace de noms sid
la source
15
Voulez-vous dire "pour une spécialisation de modèle de CLASSE explicite". Dans ce cas, cela couvrira-t-il toutes les fonctions de la classe basée sur des modèles?
Arthur
@ Arthur ne semble pas, j'ai quelques méthodes de modèle qui restent dans l'en-tête et la plupart des autres méthodes dans cpp, ça marche bien. Très belle solution.
user1633272
Dans le cas du demandeur, ils ont un modèle de fonction, pas un modèle de classe.
user253751
23

Ce code est bien formé. Il suffit de faire attention à ce que la définition du modèle soit visible au moment de l'instanciation. Pour citer la norme, § 14.7.2.4:

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

Konrad Rudolph
la source
2
Que signifie non exporté ?
Dan Nissenbaum
1
@Dan Visible uniquement à l'intérieur de son unité de compilation, pas à l'extérieur. Si vous liez plusieurs unités de compilation ensemble, les symboles exportés peuvent être utilisés entre elles (et doivent avoir une seule, ou au moins, dans le cas des modèles, des définitions cohérentes, sinon vous exécutez dans UB).
Konrad Rudolph
Merci. Je pensais que toutes les fonctions sont (par défaut) visibles en dehors de l'unité de compilation. Si j'ai deux unités de compilation a.cpp(définissant la fonction a() {}) et b.cpp(définissant la fonction b() { a() }), alors cela sera correctement lié. Si j'ai raison, la citation ci-dessus semble ne pas s'appliquer au cas typique ... je me trompe quelque part?
Dan Nissenbaum
@Dan Trivial counterexample: inlinefonctions
Konrad Rudolph
1
Les modèles de fonction @Dan le sont implicitement inline. La raison étant que sans un ABI C ++ standardisé, il est difficile / impossible de définir l'effet que cela aurait autrement.
Konrad Rudolph
15

Cela devrait fonctionner correctement partout où les modèles sont pris en charge. L'instanciation de modèle explicite fait partie de la norme C ++.

l'ombre de la lune
la source
13

Votre exemple est correct mais pas très portable. Il existe également une syntaxe légèrement plus propre qui peut être utilisée (comme indiqué par @ namespace-sid).

Supposons que la classe basée sur des modèles fasse partie d'une bibliothèque à partager. Faut-il compiler d'autres versions de la classe basée sur des modèles? Le responsable de la bibliothèque est-il censé anticiper toutes les utilisations possibles du modèle de la classe?

Une autre approche est une légère variation de ce que vous avez: ajoutez un troisième fichier qui est le fichier d'implémentation / d'instanciation du modèle.

fichier foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

fichier foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

Fichier foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

La seule mise en garde est que vous devez dire au compilateur de compiler foo-impl.cppau lieu defoo.cpp compiler ce dernier ne fait rien.

Bien sûr, vous pouvez avoir plusieurs implémentations dans le troisième fichier ou avoir plusieurs fichiers d'implémentation pour chaque type que vous souhaitez utiliser.

Cela permet beaucoup plus de flexibilité lors du partage de la classe basée sur des modèles pour d'autres utilisations.

Cette configuration réduit également les temps de compilation pour les classes réutilisées car vous ne recompilez pas le même fichier d'en-tête dans chaque unité de traduction.

Cameron Tacklind
la source
qu'est-ce que cela vous rapporte? Vous devez toujours éditer foo-impl.cpp pour ajouter une nouvelle spécialisation.
MK.
Séparation des détails d'implémentation (alias définitions dans foo.cpp) à partir desquels les versions sont réellement compilées (en foo-impl.cpp) et déclarations (en foo.h). Je n'aime pas que la plupart des modèles C ++ soient entièrement définis dans des fichiers d'en-tête. Cela est contraire à la norme C / C ++ de paires de c[pp]/hpour chaque classe / espace de noms / quel que soit le groupe que vous utilisez. Les gens semblent toujours utiliser des fichiers d'en-tête monolithiques simplement parce que cette alternative n'est pas largement utilisée ou connue.
Cameron Tacklind
1
@MK. Je mettais d'abord les instanciations explicites du modèle à la fin de la définition dans le fichier source jusqu'à ce que j'aie besoin d'autres instanciations ailleurs (par exemple, des tests unitaires utilisant une maquette comme type de modèle). Cette séparation me permet d'ajouter plus d'instanciations en externe. De plus, cela fonctionne toujours lorsque je garde l'original en tant que h/cpppaire, même si je devais entourer la liste d'origine des instanciations dans un garde d'inclusion, mais je pouvais toujours compiler le foo.cppcomme d'habitude. Cependant, je suis encore assez nouveau en C ++ et je serais intéressé de savoir si cette utilisation mixte a une mise en garde supplémentaire.
Thirdwater
3
Je pense qu'il est préférable de découpler foo.cppet foo-impl.cpp. Ne pas #include "foo.cpp"dans le foo-impl.cppfichier; au lieu de cela, ajoutez la déclaration extern template class foo<int>;à foo.cpppour empêcher le compilateur d'instancier le modèle lors de la compilation foo.cpp. Assurez-vous que le système de génération crée les deux .cppfichiers et transmet les deux fichiers objet à l'éditeur de liens. Cela présente de multiples avantages: a) il est clair foo.cppqu'il n'y a pas d'instanciation; b) les modifications de foo.cpp ne nécessitent pas de recompilation de foo-impl.cpp.
Shmuel Levine
3
Il s'agit d'une très bonne approche du problème des définitions de modèles qui prend le meilleur des deux mondes - implémentation d'en-tête et instanciation pour les types fréquemment utilisés. Le seul changement que j'apporterais à cette configuration est de renommer foo.cppen foo_impl.het foo-impl.cppen juste foo.cpp. J'ajouterais également des typedefs pour les instanciations de foo.cppà foo.h, de même using foo_int = foo<int>;. L'astuce consiste à fournir aux utilisateurs deux interfaces d'en-tête pour un choix. Lorsque l'utilisateur a besoin d'une instanciation prédéfinie, il inclut foo.h, lorsque l'utilisateur a besoin de quelque chose de désordonné, il inclut foo_impl.h.
Wormer
5

Ce n'est certainement pas un hack désagréable, mais sachez que vous devrez le faire (la spécialisation de modèle explicite) pour chaque classe / type que vous souhaitez utiliser avec le modèle donné. Dans le cas de nombreux types de demande d'instanciation de modèle, il peut y avoir BEAUCOUP de lignes dans votre fichier .cpp. Pour résoudre ce problème, vous pouvez avoir un TemplateClassInst.cpp dans chaque projet que vous utilisez afin d'avoir un meilleur contrôle sur les types qui seront instanciés. Évidemment, cette solution ne sera pas parfaite (alias la balle d'argent) car vous pourriez finir par casser l'ODR :).

Rouge XIII
la source
Êtes-vous certain que cela brisera l'ODR? Si les lignes d'instanciation dans TemplateClassInst.cpp font référence au fichier source identique (contenant les définitions de fonction de modèle), cela ne garantit-il pas de ne pas violer l'ODR puisque toutes les définitions sont identiques (même si elles sont répétées)?
Dan Nissenbaum
S'il vous plaît, qu'est-ce que l'ODR?
inamovible
4

Il existe, dans la dernière norme, un mot-clé ( export) qui aiderait à résoudre ce problème, mais il n'est implémenté dans aucun compilateur que je connaisse, à part Comeau.

Voir la FAQ à ce sujet.

Ben Collins
la source
2
AFAIK, l'exportation est morte car ils sont confrontés à des problèmes de plus en plus récents, chaque fois qu'ils résolvent le dernier, ce qui rend la solution globale de plus en plus compliquée. Et le mot-clé "export" ne vous permettra pas "d'exporter" de toute façon (toujours de H. Sutter). Alors je dis: ne retenez pas votre souffle ...
paercebal
2
Pour implémenter l'exportation, le compilateur nécessite toujours la définition complète du modèle. Tout ce que vous gagnez est de l'avoir sous une forme de compilation. Mais ça ne sert à rien.
Zan Lynx
2
... et c'est passé de la norme, en raison de complications excessives pour un gain minimal.
DevSolar
4

Il s'agit d'un moyen standard de définir des fonctions de modèle. Je pense qu'il y a trois méthodes que je lis pour définir des modèles. Ou probablement 4. Chacun avec des avantages et des inconvénients.

  1. Définissez dans la définition de classe. Je n'aime pas ça du tout parce que je pense que les définitions de classe sont strictement pour référence et devraient être faciles à lire. Cependant, il est beaucoup moins compliqué de définir des modèles en classe qu'à l'extérieur. Et toutes les déclarations de modèle ne sont pas au même niveau de complexité. Cette méthode fait également du modèle un véritable modèle.

  2. Définissez le modèle dans le même en-tête, mais en dehors de la classe. C'est ma façon préférée la plupart du temps. Il garde votre définition de classe bien rangée, le modèle reste un vrai modèle. Cependant, cela nécessite une dénomination complète du modèle, ce qui peut être délicat. De plus, votre code est accessible à tous. Mais si vous avez besoin que votre code soit en ligne, c'est la seule façon. Vous pouvez également accomplir cela en créant un fichier .INL à la fin de vos définitions de classe.

  3. Incluez le header.h et la mise en œuvre.CPP dans votre main.CPP. Je pense que c'est comme ça que c'est fait. Vous n'aurez pas à préparer de pré-instanciations, il se comportera comme un vrai modèle. Le problème que j'ai avec ça, c'est que ce n'est pas naturel. Normalement, nous n'incluons pas et nous prévoyons d'inclure des fichiers source. Je suppose que puisque vous avez inclus le fichier source, les fonctions du modèle peuvent être intégrées.

  4. Cette dernière méthode, qui était la méthode publiée, définit les modèles dans un fichier source, tout comme le numéro 3; mais au lieu d'inclure le fichier source, nous pré-instancions les modèles à ceux dont nous aurons besoin. Je n'ai aucun problème avec cette méthode et elle est parfois utile. Nous avons un gros code, il ne peut pas bénéficier d'être intégré, alors mettez-le simplement dans un fichier CPP. Et si nous connaissons des instanciations communes et que nous pouvons les prédéfinir. Cela nous évite d'écrire essentiellement la même chose 5, 10 fois. Cette méthode a l'avantage de garder notre code propriétaire. Mais je ne recommande pas de mettre de minuscules fonctions régulièrement utilisées dans les fichiers CPP. Cela réduira les performances de votre bibliothèque.

Remarque, je ne suis pas au courant des conséquences d'un fichier obj gonflé.

Cássio Renan
la source
3

Oui, c'est la manière standard de faire une instanciation explicite de spécialisation . Comme vous l'avez indiqué, vous ne pouvez pas instancier ce modèle avec d'autres types.

Edit: corrigé en fonction du commentaire.

Lou Franco
la source
Être pointilleux sur la terminologie, c'est une "instanciation explicite".
Richard Corden
2

Prenons un exemple, disons pour une raison quelconque que vous voulez avoir une classe de modèle:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Si vous compilez ce code avec Visual Studio - cela fonctionne immédiatement. gcc produira une erreur de l'éditeur de liens (si le même fichier d'en-tête est utilisé à partir de plusieurs fichiers .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Il est possible de déplacer l'implémentation vers un fichier .cpp, mais vous devez ensuite déclarer une classe comme celle-ci -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

Et puis .cpp ressemblera à ceci:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Sans deux dernières lignes dans le fichier d'en-tête - gcc fonctionnera bien, mais Visual studio produira une erreur:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

la syntaxe de classe de modèle est facultative dans le cas si vous souhaitez exposer la fonction via l'exportation .dll, mais cela ne s'applique qu'à la plate-forme Windows - donc test_template.h pourrait ressembler à ceci:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

avec le fichier .cpp de l'exemple précédent.

Cependant, cela donne plus de maux de tête au lieur, il est donc recommandé d'utiliser l'exemple précédent si vous n'exportez pas la fonction .dll.

TarmoPikaro
la source
1

Il est temps pour une mise à jour! Créez un fichier en ligne (.inl, ou probablement tout autre) et copiez simplement toutes vos définitions dedans. Assurez-vous d'ajouter le modèle au-dessus de chaque fonction ( template <typename T, ...>). Maintenant, au lieu d'inclure le fichier d'en-tête dans le fichier en ligne, vous faites le contraire. Incluez le fichier en ligne après la déclaration de votre classe ( #include "file.inl").

Je ne sais pas vraiment pourquoi personne n'a mentionné cela. Je ne vois aucun inconvénient immédiat.

Didii
la source
25
L'inconvénient immédiat est qu'il est fondamentalement le même que la simple définition des fonctions de modèle directement dans l'en-tête. Une fois que vous #include "file.inl", le préprocesseur va coller le contenu de file.inldirectement dans l'en-tête. Quelle que soit la raison pour laquelle vous vouliez éviter que l'implémentation ne passe dans l'en-tête, cette solution ne résout pas ce problème.
Cody Gray
5
- et signifie que vous êtes, techniquement et inutilement, en train de vous surcharger avec la tâche d'écrire tous les passe-partout verbeux et hallucinants nécessaires aux templatedéfinitions hors ligne . Je comprends pourquoi les gens veulent le faire - pour atteindre la plus grande parité avec les déclarations / définitions non-modèles, pour garder la déclaration d'interface en ordre, etc. - mais cela ne vaut pas toujours la peine. Il s'agit d'évaluer les compromis des deux côtés et de choisir le moins mauvais . ... jusqu'à ce namespace classque ça devienne une chose: O [ s'il vous plaît soyez une chose ]
underscore_d
2
@Andrew Il semble s'être coincé dans les tuyaux du Comité, bien que je pense avoir vu quelqu'un dire que ce n'était pas intentionnel. Je souhaite qu'il ait fait en C ++ 17. Peut-être la prochaine décennie.
underscore_d
@CodyGray: Techniquement, c'est en effet la même chose pour le compilateur et cela ne réduit donc pas le temps de compilation. Je pense que cela vaut la peine d'être mentionné et pratiqué dans un certain nombre de projets que j'ai vus. Descendre dans cette voie permet de séparer Interface de la définition, ce qui est une bonne pratique. Dans ce cas, cela n'aide pas avec la compatibilité ABI ou similaire, mais cela facilite la lecture et la compréhension de l'interface.
kiloalphaindia
0

Il n'y a rien de mal avec l'exemple que vous avez donné. Mais je dois dire que je pense qu'il n'est pas efficace de stocker des définitions de fonctions dans un fichier cpp. Je comprends seulement la nécessité de séparer la déclaration et la définition de la fonction.

Lorsqu'elle est utilisée avec une instanciation de classe explicite, la bibliothèque de vérification de concept Boost (BCCL) peut vous aider à générer du code de fonction de modèle dans des fichiers cpp.

Benoît
la source
8
Qu'est-ce qui est inefficace?
Cody Gray
0

Rien de ce qui précède n'a fonctionné pour moi, alors voici comment y résolu, ma classe n'a qu'une seule méthode basée sur des modèles ..

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

cela évite les erreurs de l'éditeur de liens, et pas besoin d'appeler du tout TemporaryFunction

KronuZ
la source