Instanciation de modèle explicite - quand est-elle utilisée?


95

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.

Réponses:


67

Directement copié à partir de https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

Vous pouvez utiliser une instanciation explicite pour créer une instanciation d'une classe ou d'une fonction basée sur un modèle sans réellement l'utiliser dans votre code. Étant donné que cela est utile lorsque vous créez des fichiers de bibliothèque (.lib) qui utilisent des modèles pour la distribution, les définitions de modèle non instanciées ne sont pas placées dans des fichiers objet (.obj).

(Par exemple, libstdc ++ contient l'instanciation explicite de std::basic_string<char,char_traits<char>,allocator<char> >(qui est std::string) donc à chaque fois que vous utilisez des fonctions de std::string, le même code de fonction n'a pas besoin d'être copié dans les objets. Le compilateur n'a besoin que de renvoyer (lier) ceux-ci à libstdc ++.)


8
Oui, les bibliothèques MSVC CRT ont des instanciations explicites pour toutes les classes stream, locale et string, spécialisées pour char et wchar_t. Le .lib résultant est supérieur à 5 mégaoctets.
Hans Passant

4
Comment le compilateur sait-il que le modèle a été explicitement instancié ailleurs? Ne va-t-il pas simplement générer la définition de classe parce qu'elle est disponible?

@STing: Si le modèle est instancié, il y aura une entrée de ces fonctions dans la table des symboles.
kennytm

@Kenny: Vous voulez dire s'il est déjà instancié dans la même TU? Je suppose que tout compilateur est suffisamment intelligent pour ne pas instancier la même spécialisation plus d'une fois dans la même TU. Je pensais que l'avantage de l'instanciation explicite (en ce qui concerne les temps de construction / liaison) est que si une spécialisation est (explicitement) instanciée dans une TU, elle ne sera pas instanciée dans les autres TU dans lesquelles elle est utilisée. Non?

4
@Kenny: Je connais l'option GCC pour empêcher l'instanciation implicite, mais ce n'est pas une norme. Autant que je sache, VC ++ n'a pas une telle option. Explicit inst. est toujours présenté comme une amélioration des temps de compilation / liaison (même par Bjarne), mais pour que cela puisse servir cet objectif, le compilateur doit en quelque sorte savoir qu'il ne faut pas instancier implicitement les modèles (par exemple, via le drapeau GCC), ou ne doit pas recevoir le définition de modèle, seulement une déclaration. Cela vous semble-t-il correct? J'essaie juste de comprendre pourquoi on utiliserait une instanciation explicite (autre que pour limiter les types concrets).

85

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();
}

1
Est - il exact de dire que si le compilateur a toute la définition du modèle (y compris les définitions de fonction) dans une unité de traduction donnée, il sera instancier une spécialisation du modèle en cas de besoin (indépendamment du fait que cette spécialisation a été explicitement instancié dans un autre TU)? C'est-à-dire, afin de récolter les avantages de la compilation / du moment de la liaison de l'instanciation explicite, il faut seulement inclure la déclaration du modèle afin que le compilateur ne puisse pas l' instancier?

1
@ user123456: probablement dépendant du compilateur. Mais plus que probablement vrai dans la plupart des situations.
Martin York

1
y a-t-il un moyen de faire en sorte que le compilateur utilise cette version explicitement instanciée pour les types que vous pré-spécifiez, mais si vous essayez d'instancier le modèle avec un type "bizarre / inattendu", faites-le fonctionner "normalement", où il instancie le modèle selon les besoins?
David Doria

2
quel serait un bon contrôle / test pour s'assurer que les instanciations explicites sont effectivement utilisées? C'est-à-dire que cela fonctionne, mais je ne suis pas entièrement convaincu qu'il ne s'agit pas simplement d'instancier tous les modèles à la demande.
David Doria

7
La plupart des discussions de commentaires ci-dessus ne sont plus vraies depuis c ++ 11: Une déclaration d'instanciation explicite (un modèle externe) empêche les instanciations implicites: le code qui provoquerait autrement une instanciation implicite doit utiliser la définition d'instanciation explicite fournie ailleurs dans le programme (généralement, dans un autre fichier: cela peut être utilisé pour réduire les temps de compilation) en.cppreference.com/w/cpp/language/class_template
xaxxon

21

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 pour empêcher les outils de génération de reconstruire les inclusions
  • redéfinition d'objet

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.cppet notmain.cppimplicitement définissent en MyTemplate<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 cela Wsignifie 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 a MyTemplate<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 section
  • 0000000000000017: taille du code généré pour eux

Nous pouvons le voir un peu plus clairement avec:

objdump -S main.o | c++filt

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 _ZN10MyTemplateIiE1fEiest le nom mutilé deMyTemplate<int>::f(int)> dont a c++filtdé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:

    • si vous êtes une bibliothèque d'en-tête uniquement, vous forcez les projets externes à effectuer leur propre instanciation explicite. Si vous n'êtes pas une bibliothèque avec en-tête uniquement, cette solution est probablement la meilleure.
    • si le type de modèle est défini dans votre propre projet et non comme un type intégré 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, nmcontient 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.oune compilation de MyTemplate<int>comme souhaité, tandis que notmain.oet main.one pas parce que Usignifie 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:

  • accélérer la compilation de votre projet
  • exposer les en-têtes en tant qu'API de bibliothèque externe pour que d'autres puissent l'utiliser

alors vous pouvez essayer l'une des solutions suivantes:

    • mytemplate.hpp: définition du modèle
    • mytemplate_interface.hpp: modèle de déclaration correspondant uniquement aux définitions de mytemplate_interface.hpp, pas de définitions
    • mytemplate.cpp: inclure mytemplate.hppet créer des instancitations explicites
    • main.cppet partout ailleurs dans la base de code: inclure mytemplate_interface.hpp, pasmytemplate.hpp
    • mytemplate.hpp: définition du modèle
    • mytemplate_implementation.hpp: inclut mytemplate.hppet ajoute externà chaque classe qui sera instanciée
    • mytemplate.cpp: inclure mytemplate.hppet créer des instancitations explicites
    • main.cppet partout ailleurs dans la base de code: inclure mytemplate_implementation.hpp, pasmytemplate.hpp

Ou encore mieux peut-être pour plusieurs en-têtes: créez un dossier intf/ impldans votre includes/dossier et utilisez mytemplate.hpptoujours comme nom.

L' mytemplate_interface.hppapproche 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.oré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 -Csur l'exécutable final et de grep le nom du modèle:

nm -C main.out | grep AnimalTemplate

ce qui indique directement que Dogc'était l'une des instanciations:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.