Après quelques semaines de pause, j'essaie d'élargir et d'étendre mes connaissances des modèles avec le livre Templates - The Complete Guide de David Vandevoorde et Nicolai M. Josuttis, et ce que j'essaie de comprendre en ce moment, c'est l'instanciation explicite des modèles .
Je n'ai pas vraiment de problème avec le mécanisme en tant que tel, mais je ne peux pas imaginer une situation dans laquelle je voudrais ou veux utiliser cette fonctionnalité. Si quelqu'un peut m'expliquer cela, je serai plus que reconnaissant.
Si vous définissez une classe de modèle que vous souhaitez uniquement utiliser pour quelques types explicites.
Placez la déclaration de modèle dans le fichier d'en-tête comme une classe normale.
Placez la définition du modèle dans un fichier source comme une classe normale.
Ensuite, à la fin du fichier source, instanciez explicitement uniquement la version que vous souhaitez rendre disponible.
Exemple idiot:
// StringAdapter.h template<typename T> class StringAdapter { public: StringAdapter(T* data); void doAdapterStuff(); private: std::basic_string<T> m_data; }; typedef StringAdapter<char> StrAdapter; typedef StringAdapter<wchar_t> WStrAdapter;
La source:
// StringAdapter.cpp #include "StringAdapter.h" template<typename T> StringAdapter<T>::StringAdapter(T* data) :m_data(data) {} template<typename T> void StringAdapter<T>::doAdapterStuff() { /* Manipulate a string */ } // Explicitly instantiate only the classes you want to be defined. // In this case I only want the template to work with characters but // I want to support both char and wchar_t with the same code. template class StringAdapter<char>; template class StringAdapter<wchar_t>;
Principale
#include "StringAdapter.h" // Note: Main can not see the definition of the template from here (just the declaration) // So it relies on the explicit instantiation to make sure it links. int main() { StrAdapter x("hi There"); x.doAdapterStuff(); }
la source
L'instanciation explicite permet de réduire les temps de compilation et la taille des objets
Ce sont les gains majeurs qu'il peut apporter. Ils proviennent des deux effets suivants décrits en détail dans les sections ci-dessous:
Supprimer les définitions des en-têtes
L'instanciation explicite vous permet de laisser des définitions dans le fichier .cpp.
Lorsque la définition est sur l'en-tête et que vous la modifiez, un système de construction intelligent recompilerait tous les inclusions, ce qui pourrait être des dizaines de fichiers, rendant la compilation insupportablement lente.
Mettre des définitions dans des fichiers .cpp présente l'inconvénient que les bibliothèques externes ne peuvent pas réutiliser le modèle avec leurs propres nouvelles classes, mais «Supprimer les définitions des en-têtes inclus mais aussi exposer les modèles à une API externe» ci-dessous montre une solution de contournement.
Voir des exemples concrets ci-dessous.
Gains de redéfinition d'objet: comprendre le problème
Si vous définissez simplement complètement un modèle sur un fichier d'en-tête, chaque unité de compilation qui inclut cet en-tête finit par compiler sa propre copie implicite du modèle pour chaque utilisation d'argument de modèle différente.
Cela signifie beaucoup d'utilisation inutile du disque et de temps de compilation.
Voici un exemple concret, dans lequel les deux
main.cpp
etnotmain.cpp
implicitement définissent enMyTemplate<int>
raison de son utilisation dans ces fichiers.main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t) { return t + 1; } }; #endif
notmain.hpp
#ifndef NOTMAIN_HPP #define NOTMAIN_HPP int notmain(); #endif
GitHub en amont .
Compilez et affichez les symboles avec
nm
:g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o echo notmain.o nm -C -S notmain.o | grep MyTemplate echo main.o nm -C -S main.o | grep MyTemplate
Production:
notmain.o 0000000000000000 0000000000000017 W MyTemplate<int>::f(int) main.o 0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
De
man nm
, nous voyons que celaW
signifie un symbole faible, que GCC a choisi car il s'agit d'une fonction de modèle. Le symbole faible signifie que le code généré implicitement pour aMyTemplate<int>
été compilé sur les deux fichiers.La raison pour laquelle il n'explose pas au moment de la liaison avec plusieurs définitions est que l'éditeur de liens accepte plusieurs définitions faibles, et en choisit simplement une à mettre dans l'exécutable final.
Les nombres dans la sortie signifient:
0000000000000000
: adresse dans la section. Ce zéro est dû au fait que les modèles sont automatiquement placés dans leur propre section0000000000000017
: taille du code généré pour euxNous pouvons le voir un peu plus clairement avec:
qui se termine par:
Disassembly of section .text._ZN10MyTemplateIiE1fEi: 0000000000000000 <MyTemplate<int>::f(int)>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 89 7d f8 mov %rdi,-0x8(%rbp) c: 89 75 f4 mov %esi,-0xc(%rbp) f: 8b 45 f4 mov -0xc(%rbp),%eax 12: 83 c0 01 add $0x1,%eax 15: 5d pop %rbp 16: c3 retq
et
_ZN10MyTemplateIiE1fEi
est le nom mutilé deMyTemplate<int>::f(int)>
dont ac++filt
décidé de ne pas démêler.Nous voyons donc qu'une section distincte est générée pour chaque instanciation de méthode unique, et que chacune d'elles prend bien sûr de l'espace dans les fichiers objets.
Solutions au problème de redéfinition des objets
Ce problème peut être évité en utilisant une instanciation explicite et soit:
garder la définition sur hpp et ajouter
extern template
hpp pour les types qui vont être explicitement instanciés.Comme expliqué à: utilisation d'un modèle externe (C ++ 11)
extern template
empêche un modèle complètement défini d'être instancié par des unités de compilation, à l'exception de notre instanciation explicite. De cette façon, seule notre instanciation explicite sera définie dans les objets finaux:mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t) { return t + 1; } }; extern template class MyTemplate<int>; #endif
mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation required just for int. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" int notmain() { return MyTemplate<int>().f(1); }
Inconvénient:
int
, il semble que vous soyez obligé d'ajouter l'inclusion pour celui-ci sur l'en-tête, une déclaration avant ne suffit pas: modèle externe & types incomplets Cela augmente les dépendances d'en-tête un peu.déplacer la définition sur le fichier cpp, ne laisser que la déclaration sur hpp, c'est-à-dire modifier l'exemple d'origine pour qu'il soit:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t); }; #endif
mytemplate.cpp
#include "mytemplate.hpp" template<class T> T MyTemplate<T>::f(T t) { return t + 1; } // Explicit instantiation. template class MyTemplate<int>;
Inconvénient: les projets externes ne peuvent pas utiliser votre modèle avec leurs propres types. Vous êtes également obligé d'instancier explicitement tous les types. Mais c'est peut-être un avantage puisque les programmeurs n'oublieront pas.
garder la définition sur hpp et ajouter
extern template
sur chaque inclus:mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" // extern template declaration extern template class MyTemplate<int>; int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" // extern template declaration extern template class MyTemplate<int>; int notmain() { return MyTemplate<int>().f(1); }
Inconvénient: tous les inclus doivent ajouter le
extern
à leurs fichiers CPP, ce que les programmeurs oublieront probablement de faire.Avec l'une de ces solutions,
nm
contient désormais:notmain.o U MyTemplate<int>::f(int) main.o U MyTemplate<int>::f(int) mytemplate.o 0000000000000000 W MyTemplate<int>::f(int)
nous voyons donc avoir
mytemplate.o
une compilation deMyTemplate<int>
comme souhaité, tandis quenotmain.o
etmain.o
ne pas parce queU
signifie indéfini.Supprimez les définitions des en-têtes inclus, mais exposez également les modèles d'une API externe dans une bibliothèque d'en-tête uniquement
Si votre bibliothèque n'est pas uniquement en-tête, le
extern template
méthode fonctionnera, car l'utilisation de projets sera simplement liée à votre fichier objet, qui contiendra l'objet de l'instanciation de modèle explicite.Cependant, pour les bibliothèques d'en-tête uniquement, si vous souhaitez les deux:
alors vous pouvez essayer l'une des solutions suivantes:
mytemplate.hpp
: définition du modèlemytemplate_interface.hpp
: modèle de déclaration correspondant uniquement aux définitions demytemplate_interface.hpp
, pas de définitionsmytemplate.cpp
: incluremytemplate.hpp
et créer des instancitations explicitesmain.cpp
et partout ailleurs dans la base de code: incluremytemplate_interface.hpp
, pasmytemplate.hpp
mytemplate.hpp
: définition du modèlemytemplate_implementation.hpp
: inclutmytemplate.hpp
et ajouteextern
à chaque classe qui sera instanciéemytemplate.cpp
: incluremytemplate.hpp
et créer des instancitations explicitesmain.cpp
et partout ailleurs dans la base de code: incluremytemplate_implementation.hpp
, pasmytemplate.hpp
Ou encore mieux peut-être pour plusieurs en-têtes: créez un dossier
intf
/impl
dans votreincludes/
dossier et utilisezmytemplate.hpp
toujours comme nom.L'
mytemplate_interface.hpp
approche ressemble à ceci:mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP #include "mytemplate_interface.hpp" template<class T> T MyTemplate<T>::f(T t) { return t + 1; } #endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP #define MYTEMPLATE_INTERFACE_HPP template<class T> struct MyTemplate { T f(T t); }; #endif
mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate_interface.hpp" int main() { std::cout << MyTemplate<int>().f(1) << std::endl; }
Compilez et exécutez:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Production:
2
Testé dans Ubuntu 18.04.
Modules C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Je pense que cette fonctionnalité fournira la meilleure configuration à mesure qu'elle deviendra disponible, mais je ne l'ai pas encore vérifiée car elle n'est pas encore disponible sur mon GCC 9.2.1.
Vous devrez toujours faire une instanciation explicite pour obtenir l'accélération / la sauvegarde du disque, mais au moins nous aurons une solution sensée pour "Supprimer les définitions des en-têtes inclus mais aussi exposer les modèles d'une API externe" qui ne nécessite pas de copier les choses environ 100 fois.
Utilisation attendue (sans l'insantiation explicite, vous ne savez pas à quoi ressemblera la syntaxe exacte, voir: Comment utiliser l'instanciation explicite de modèle avec des modules C ++ 20? )
helloworld.cpp
export module helloworld; // module declaration import <iostream>; // import declaration template<class T> export void hello(T t) { // export declaration std::cout << t << std::end; }
main.cpp
import helloworld; // import declaration int main() { hello(1); hello("world"); }
puis compilation mentionnée sur https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm clang++ -std=c++2a -c -o helloworld.o helloworld.cpp clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Donc, à partir de cela, nous voyons que clang peut extraire l'interface du modèle + l'implémentation dans la magie
helloworld.pcm
, qui doit contenir une représentation intermédiaire LLVM de la source: Comment les modèles sont-ils gérés dans le système de modules C ++? ce qui permet toujours la spécification du modèle.Comment analyser rapidement votre build pour voir s'il gagnerait beaucoup à l'instanciation de modèle
Donc, vous avez un projet complexe et vous voulez décider si l'instanciation du modèle apportera des gains significatifs sans faire le refactor complet?
L'analyse ci-dessous peut vous aider à décider, ou du moins à sélectionner les objets les plus prometteurs à refactoriser en premier pendant que vous expérimentez, en empruntant quelques idées à: Mon fichier objet C ++ est trop gros
# List all weak symbols with size only, no address. find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' | grep ' W ' > nm.log # Sort by symbol size. sort -k1 -n nm.log -o nm.sort.log # Get a repetition count. uniq -c nm.sort.log > nm.uniq.log # Find the most repeated/largest objects. sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log # Find the objects that would give you the most gain after refactor. # This gain is calculated as "(n_occurences - 1) * size" which is # the size you would gain for keeping just a single instance. # If you are going to refactor anything, you should start with the ones # at the bottom of this list. awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log | sort -k1 -n > nm.gains.log # Total gain if you refactored everything. awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log # Total size. The closer total gain above is to total size, the more # you would gain from the refactor. awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
Le rêve: un cache de compilateur de modèles
Je pense que la solution ultime serait de pouvoir construire avec:
g++ --template-cache myfile.o file1.cpp g++ --template-cache myfile.o file2.cpp
puis
myfile.o
réutiliserait automatiquement les modèles précédemment compilés dans les fichiers.Cela signifierait 0 effort supplémentaire sur les programmeurs en plus de transmettre cette option CLI supplémentaire à votre système de construction.
Un bonus secondaire de l'instanciation de modèle explicite: aidez les IDE à répertorier les instanciations de modèle
J'ai constaté que certains IDE tels qu'Eclipse ne peuvent pas résoudre "une liste de toutes les instanciations de modèles utilisées".
Ainsi, par exemple, si vous êtes à l'intérieur d'un code basé sur un modèle et que vous voulez trouver les valeurs possibles du modèle, vous devrez trouver les utilisations du constructeur un par un et en déduire les types possibles un par un.
Mais sur Eclipse 2020-03, je peux facilement lister les modèles explicitement instanciés en effectuant une recherche Find all usages (Ctrl + Alt + G) sur le nom de la classe, ce qui me pointe par exemple à partir de:
template <class T> struct AnimalTemplate { T animal; AnimalTemplate(T animal) : animal(animal) {} std::string noise() { return animal.noise(); } };
à:
template class AnimalTemplate<Dog>;
Voici une démo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Une autre technique de guérilla que vous pourriez utiliser en dehors de l'EDI serait de s'exécuter
nm -C
sur l'exécutable final et de grep le nom du modèle:ce qui indique directement que
Dog
c'était l'une des instanciations:0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]() 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog) 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
la source
Cela dépend du modèle du compilateur - apparemment, il y a le modèle Borland et le modèle CFront. Et puis cela dépend aussi de votre intention - si vous écrivez une bibliothèque, vous pouvez (comme évoqué ci-dessus) instancier explicitement les spécialisations que vous souhaitez.
La page GNU c ++ traite des modèles ici https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .
la source