Istanziazione esplicita del modello: quando viene utilizzata?


95

Dopo alcune settimane di pausa, sto cercando di espandere ed estendere la mia conoscenza dei modelli con il libro Templates - The Complete Guide di David Vandevoorde e Nicolai M. Josuttis, e quello che sto cercando di capire in questo momento è un'istanza esplicita dei modelli .

In realtà non ho problemi con il meccanismo in quanto tale, ma non riesco a immaginare una situazione in cui vorrei o voglio utilizzare questa funzione. Se qualcuno me lo può spiegare, sarò più che grato.

Risposte:


67

Copiato direttamente da https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

È possibile utilizzare l'istanza esplicita per creare un'istanza di una classe o funzione basata su modelli senza utilizzarla effettivamente nel codice. Poiché ciò è utile quando si creano file di libreria (.lib) che utilizzano modelli per la distribuzione, le definizioni di modello non istanziate non vengono inserite nei file oggetto (.obj).

(Ad esempio, libstdc ++ contiene l'istanza esplicita di std::basic_string<char,char_traits<char>,allocator<char> >(che è std::string) quindi ogni volta che usi le funzioni di std::string, lo stesso codice funzione non deve essere copiato negli oggetti. Il compilatore deve solo fare riferimento (collegare) quelli a libstdc ++.)


8
Sì, le librerie MSVC CRT hanno istanze esplicite per tutte le classi stream, locale e string, specializzate per char e wchar_t. Il .lib risultante supera i 5 megabyte.
Hans Passant

4
Come fa il compilatore a sapere che il modello è stato esplicitamente istanziato altrove? Non genererà solo la definizione della classe perché è disponibile?

@STing: se viene creata un'istanza del modello, ci sarà una voce di quelle funzioni nella tabella dei simboli.
kennytm

@ Kenny: Vuoi dire se è già istanziato nella stessa TU? Presumo che qualsiasi compilatore sia abbastanza intelligente da non istanziare la stessa specializzazione più di una volta nella stessa TU. Ho pensato che il vantaggio di un'istanza esplicita (per quanto riguarda i tempi di compilazione / collegamento) è che se una specializzazione è (esplicitamente) istanziata in una TU, non sarà istanziata nelle altre TU in cui viene utilizzata. No?

4
@ Kenny: so dell'opzione GCC per impedire l'istanza implicita, ma questo non è uno standard. Per quanto ne so, VC ++ non ha tale opzione. Inst esplicito è sempre pubblicizzato come un miglioramento dei tempi di compilazione / collegamento (anche da Bjarne), ma affinché possa servire a tale scopo, il compilatore deve in qualche modo sapere di non istanziare implicitamente i modelli (ad esempio, tramite il flag GCC), o non deve essere dato il definizione del modello, solo una dichiarazione. Questo suona corretto? Sto solo cercando di capire perché si dovrebbe usare un'istanza esplicita (oltre a limitare i tipi concreti).

85

Se definisci una classe modello che vuoi lavorare solo per un paio di tipi espliciti.

Metti la dichiarazione del modello nel file di intestazione proprio come una normale classe.

Metti la definizione del modello in un file sorgente proprio come una normale classe.

Quindi, alla fine del file sorgente, istanzia esplicitamente solo la versione che desideri sia disponibile.

Esempio sciocco:

// 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;

Fonte:

// 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
È corretto dire che se il compilatore ha l'intera definizione del modello (comprese le definizioni di funzioni) in una data unità di traduzione, esso sarà un'istanza di una specializzazione del modello quando necessario (a prescindere dal fatto che la specializzazione è stato esplicitamente un'istanza in un altro TU)? Cioè, per raccogliere i vantaggi in fase di compilazione / collegamento dell'istanza esplicita, è necessario includere solo la dichiarazione del modello in modo che il compilatore non possa istanziarla?

1
@ user123456: probabilmente dipendente dal compilatore. Ma molto probabilmente vero nella maggior parte delle situazioni.
Martin York

1
c'è un modo per fare in modo che il compilatore usi questa versione esplicitamente istanziata per i tipi specificati in anticipo, ma poi se provi a istanziare il modello con un tipo "strano / inaspettato", fallo funzionare "normalmente", dove è solo istanzia il modello secondo necessità?
David Doria

2
quale sarebbe un buon controllo / test per assicurarsi che le istanze esplicite siano effettivamente utilizzate? Cioè sta funzionando, ma non sono del tutto convinto che non stia solo istanziando tutti i modelli su richiesta.
David Doria

7
La maggior parte delle chiacchiere sui commenti di cui sopra non sono più vere poiché c ++ 11: una dichiarazione di istanziazione esplicita (un modello esterno) impedisce le istanze implicite: il codice che altrimenti causerebbe un'istanza implicita deve usare la definizione di istanziazione esplicita fornita da qualche altra parte nel programma (in genere, in un altro file: questo può essere utilizzato per ridurre i tempi di compilazione) en.cppreference.com/w/cpp/language/class_template
xaxxon

21

L'istanziazione esplicita consente di ridurre i tempi di compilazione e le dimensioni degli oggetti

Questi sono i principali vantaggi che può fornire. Derivano dai seguenti due effetti descritti in dettaglio nelle sezioni seguenti:

  • rimuovere le definizioni dalle intestazioni per impedire agli strumenti di compilazione di ricostruire includer
  • ridefinizione degli oggetti

Rimuovi le definizioni dalle intestazioni

L'istanziazione esplicita consente di lasciare le definizioni nel file .cpp.

Quando la definizione è sull'intestazione e la modifichi, un sistema di compilazione intelligente ricompila tutti gli includer, che potrebbero essere dozzine di file, rendendo la compilazione insopportabilmente lenta.

L'inserimento delle definizioni nei file .cpp ha lo svantaggio che le librerie esterne non possono riutilizzare il modello con le loro nuove classi, ma "Rimuovi le definizioni dalle intestazioni incluse ma esponi anche i modelli un'API esterna" di seguito mostra una soluzione alternativa.

Vedi esempi concreti di seguito.

La ridefinizione degli oggetti guadagna: comprensione del problema

Se si definisce completamente un modello su un file di intestazione, ogni singola unità di compilazione che include quell'intestazione finisce per compilare la propria copia implicita del modello per ogni diverso utilizzo dell'argomento del modello.

Ciò significa molto utilizzo inutile del disco e tempo di compilazione.

Ecco un esempio concreto, in cui entrambi main.cppe notmain.cppimplicitamente definiscono a MyTemplate<int>causa del suo utilizzo in quei file.

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 a monte .

Compila e visualizza i simboli con 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

Produzione:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Da man nm, vediamo che Wsignifica simbolo debole, che GCC ha scelto perché questa è una funzione modello. Il simbolo debole significa che il codice compilato implicitamente generato per è MyTemplate<int>stato compilato su entrambi i file.

Il motivo per cui non esplode al momento del collegamento con più definizioni è che il linker accetta più definizioni deboli e ne sceglie solo una da inserire nell'eseguibile finale.

I numeri nell'output significano:

  • 0000000000000000: indirizzo nella sezione. Questo zero è perché i modelli vengono inseriti automaticamente nella propria sezione
  • 0000000000000017: dimensione del codice generato per loro

Possiamo vederlo un po 'più chiaramente con:

objdump -S main.o | c++filt

che termina con:

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

ed _ZN10MyTemplateIiE1fEiè il nome mutilato di MyTemplate<int>::f(int)>cui ha c++filtdeciso di non districare.

Quindi vediamo che una sezione separata viene generata per ogni singola istanza di metodo e che ognuna di esse occupa ovviamente spazio nei file oggetto.

Soluzioni al problema della ridefinizione degli oggetti

Questo problema può essere evitato utilizzando un'istanza esplicita e:

  • mantieni la definizione su hpp e aggiungi extern template hpp per i tipi che verranno istanziati in modo esplicito.

    Come spiegato in: l' uso di un modello esterno (C ++ 11) extern template impedisce che un modello completamente definito venga istanziato dalle unità di compilazione, ad eccezione della nostra istanza esplicita. In questo modo, solo la nostra istanziazione esplicita sarà definita negli oggetti finali:

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

    Svantaggio:

    • se sei una libreria di sola intestazione, costringi i progetti esterni a fare la loro istanza esplicita. Se non sei una libreria di sola intestazione, questa soluzione è probabilmente la migliore.
    • se il tipo di modelloèdefinito nel tuo progetto e non un built-in come int, sembra che tu sia costretto ad aggiungere l'inclusione nell'intestazione, una dichiarazione in avanti non è sufficiente: modello esterno e tipi incompleti Ciò aumenta le dipendenze dell'intestazione un po.
  • spostando la definizione sul file cpp, lasciare solo la dichiarazione su hpp, ovvero modificare l'esempio originale in modo che sia:

    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>;
    

    Svantaggio: i progetti esterni non possono utilizzare il tuo modello con i propri tipi. Inoltre sei costretto a istanziare esplicitamente tutti i tipi. Ma forse questo è un vantaggio da allora i programmatori non dimenticheranno.

  • mantieni la definizione su hpp e aggiungi extern templateogni includer:

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

    Svantaggio: tutti gli includer devono aggiungere il externai loro file CPP, cosa che probabilmente i programmatori dimenticheranno di fare.

Con una di queste soluzioni, nmora contiene:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

quindi vediamo che ha solo mytemplate.ouna compilazione di MyTemplate<int>come desiderato, mentre notmain.oe main.onon perché Usignifica indefinito.

Rimuovi le definizioni dalle intestazioni incluse ma esponi anche i modelli di un'API esterna in una libreria di sole intestazioni

Se la tua libreria non è solo intestazione, il extern templatemetodo funzionerà, poiché l'utilizzo dei progetti si collegherà semplicemente al tuo file oggetto, che conterrà l'oggetto dell'istanza esplicita del modello.

Tuttavia, per le librerie di sola intestazione, se vuoi entrambe:

  • velocizza la compilazione del tuo progetto
  • esporre le intestazioni come API di libreria esterna per consentire ad altri di utilizzarle

quindi puoi provare uno dei seguenti:

    • mytemplate.hpp: definizione del modello
    • mytemplate_interface.hpp: dichiarazione del modello che corrisponde solo alle definizioni da mytemplate_interface.hpp, nessuna definizione
    • mytemplate.cpp: include mytemplate.hppe crea istanze esplicite
    • main.cppe ovunque nel codice base: include mytemplate_interface.hpp, notmytemplate.hpp
    • mytemplate.hpp: definizione del modello
    • mytemplate_implementation.hpp: include mytemplate.hppe aggiunge externa ogni classe che verrà istanziata
    • mytemplate.cpp: include mytemplate.hppe crea istanze esplicite
    • main.cppe ovunque nel codice base: include mytemplate_implementation.hpp, notmytemplate.hpp

O ancora meglio forse per più intestazioni: crea una cartella intf/ implall'interno della tua includes/cartella e usa sempre mytemplate.hppcome nome.

L' mytemplate_interface.hppapproccio è simile a questo:

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

Compila ed esegui:

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

Produzione:

2

Testato in Ubuntu 18.04.

Moduli C ++ 20

https://en.cppreference.com/w/cpp/language/modules

Penso che questa funzione fornirà la migliore configurazione in futuro non appena sarà disponibile, ma non l'ho ancora verificata perché non è ancora disponibile sul mio GCC 9.2.1.

Dovrai comunque eseguire un'istanza esplicita per ottenere l'accelerazione / il salvataggio del disco, ma almeno avremo una soluzione sensata per "Rimuovere le definizioni dalle intestazioni incluse ma anche esporre i modelli un'API esterna" che non richiede la copia di cose circa 100 volte.

L'utilizzo previsto (senza l'insantiazione esplicita, non sono sicuro di quale sarà la sintassi esatta, vedere: Come utilizzare l'istanza esplicita del modello con i moduli C ++ 20? ) Essere qualcosa lungo:

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

e poi compilation menzionata su 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

Quindi da questo vediamo che clang può estrarre l'interfaccia del modello + l'implementazione nella magia helloworld.pcm, che deve contenere una rappresentazione intermedia LLVM del sorgente: come vengono gestiti i modelli nel sistema di moduli C ++? che consente ancora la specifica del modello.

Come analizzare rapidamente la tua build per vedere se guadagnerebbe molto dall'istanziazione del modello

Quindi, hai un progetto complesso e vuoi decidere se l'istanziazione del modello porterà vantaggi significativi senza effettivamente eseguire il refactoring completo?

L'analisi seguente potrebbe aiutarti a decidere, o almeno a selezionare gli oggetti più promettenti da sottoporre a refactoring durante la sperimentazione, prendendo in prestito alcune idee da: Il mio file oggetto C ++ è troppo grande

# 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

Il sogno: una cache del compilatore di modelli

Penso che la soluzione definitiva sarebbe se potessimo costruire con:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

e quindi myfile.oriutilizzerebbe automaticamente i modelli precedentemente compilati tra i file.

Ciò significherebbe 0 sforzi in più per i programmatori oltre a passare quell'opzione CLI in più al tuo sistema di compilazione.

Un vantaggio secondario della creazione di istanze di modelli esplicite: aiuta gli IDE a elencare le istanze di modelli

Ho scoperto che alcuni IDE come Eclipse non possono risolvere "un elenco di tutte le istanze di modelli utilizzate".

Quindi, ad esempio, se ci si trova all'interno di un codice basato su modelli e si desidera trovare i possibili valori del modello, è necessario trovare gli usi del costruttore uno per uno e dedurre i possibili tipi uno per uno.

Ma su Eclipse 2020-03 posso facilmente elencare i modelli istanziati in modo esplicito eseguendo una ricerca Trova tutti gli usi (Ctrl + Alt + G) sul nome della classe, che mi punta ad esempio da:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

per:

template class AnimalTemplate<Dog>;

Ecco una demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Un'altra tecnica di guerriglia che potresti usare al di fuori dell'IDE, tuttavia, sarebbe quella di eseguire nm -Csull'eseguibile finale e grep il nome del modello:

nm -C main.out | grep AnimalTemplate

che punta direttamente al fatto che Dogera una delle istanze:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.