Perché i modelli possono essere implementati solo nel file di intestazione?

Citazione dalla libreria standard C ++: un tutorial e un manuale :

L’unico modo portatile di utilizzare i modelli al momento è implementarli nei file di intestazione usando le funzioni inline.

Perchè è questo?

(Chiarimento: i file header non sono l’ unica soluzione portatile, ma sono la soluzione portatile più conveniente.)

Non è necessario inserire l’implementazione nel file di intestazione, vedere la soluzione alternativa alla fine di questa risposta.

Ad ogni modo, il motivo per cui il tuo codice sta fallendo è che, quando istanzia un template, il compilatore crea una nuova class con l’argomento template specificato. Per esempio:

template struct Foo { T bar; void doSomething(T param) {/* do stuff using T */} }; // somewhere in a .cpp Foo f; 

Durante la lettura di questa riga, il compilatore creerà una nuova class (chiamiamola FooInt ), che è equivalente al seguente:

 struct FooInt { int bar; void doSomething(int param) {/* do stuff using int */} } 

Di conseguenza, il compilatore deve avere accesso all’implementazione dei metodi, per istanziarli con l’argomento template (in questo caso int ). Se queste implementazioni non fossero nell’intestazione, non sarebbero accessibili e pertanto il compilatore non sarebbe in grado di creare un’istanza del modello.

Una soluzione comune a questo è scrivere la dichiarazione del modello in un file di intestazione, quindi implementare la class in un file di implementazione (ad esempio .tpp) e includere questo file di implementazione alla fine dell’intestazione.

 // Foo.h template  struct Foo { void doSomething(T param); }; #include "Foo.tpp" // Foo.tpp template  void Foo::doSomething(T param) { //implementation } 

In questo modo, l’implementazione è ancora separata dalla dichiarazione, ma è accessibile al compilatore.

Un’altra soluzione consiste nel mantenere separata l’implementazione e creare un’istanza esplicita di tutte le istanze del modello necessarie:

 // Foo.h // no implementation template  struct Foo { ... }; //---------------------------------------- // Foo.cpp // implementation of Foo's methods // explicit instantiations template class Foo; template class Foo; // You will only be able to use Foo with int or float 

Se la mia spiegazione non è abbastanza chiara, puoi dare un’occhiata alle Super-FAQ C ++ su questo argomento .

Molte risposte corrette qui, ma volevo aggiungere questo (per completezza):

Se nella parte inferiore del file cpp di implementazione si fa l’istanziazione esplicita di tutti i tipi con cui il modello verrà usato, il linker sarà in grado di trovarli come al solito.

Modifica: aggiunta di esempi di istanze di template esplicite. Utilizzato dopo che il modello è stato definito e tutte le funzioni membro sono state definite.

 template class vector; 

Ciò istanzia (e quindi rende disponibile al linker) la class e tutte le sue funzioni membro (solo). Sintassi simile funziona per le funzioni del modello, quindi se si dispone di overload di operatori non membri potrebbe essere necessario fare lo stesso per questi.

L’esempio sopra riportato è abbastanza inutile poiché il vettore è completamente definito nelle intestazioni, tranne quando un file di inclusione comune (intestazione precompilata?) Utilizza il extern template class vector modo da impedirne l’istanziazione in tutti gli altri file (1000?) che usano il vettore.

È a causa del requisito per la compilazione separata e perché i modelli sono polimorfismi in stile di istanza.

Andiamo un po ‘più vicino al concreto per una spiegazione. Dì che ho i seguenti file:

  • foo.h
    • dichiara l’interfaccia della class MyClass
  • foo.cpp
    • definisce l’implementazione della class MyClass
  • bar.cpp
    • utilizza MyClass

Compilazione separata significa che dovrei essere in grado di compilare foo.cpp indipendentemente da bar.cpp . Il compilatore fa tutto il duro lavoro di analisi, ottimizzazione e generazione di codice su ogni unità di compilazione in modo completamente indipendente; non abbiamo bisogno di fare analisi dell’intero programma. È solo il linker che deve gestire l’intero programma in una sola volta e il lavoro del linker è sostanzialmente più semplice.

bar.cpp non ha nemmeno bisogno di esistere quando compilo foo.cpp , ma dovrei comunque essere in grado di colbind il foo.o Ho già avuto insieme alla barra . Ho appena prodotto, senza bisogno di ricompilare foo .cpp . foo.cpp potrebbe anche essere compilato in una libreria dynamic, distribuita da qualche altra parte senza foo.cpp , e collegata con il codice che scrivono anni dopo aver scritto foo.cpp .

“Polimorfismo in stile istanziato” significa che il modello MyClass non è realmente una class generica che può essere compilata in codice che può funzionare per qualsiasi valore di T Ciò aggiungerebbe sovraccarico come il pugilato, il bisogno di passare i puntatori di funzione agli allocatori e ai costruttori, ecc. L’intenzione dei modelli C ++ è di evitare di scrivere class MyClass_int quasi identiche class MyClass_int , class MyClass_float , ecc, ma di essere ancora in grado di finire codice compilato che è principalmente come se avessimo scritto ogni versione separatamente. Quindi un modello è letteralmente un modello; un modello di class non è una class, è una ricetta per creare una nuova class per ogni T che incontriamo. Un modello non può essere compilato in codice, solo il risultato dell’istanziazione del modello può essere compilato.

Quindi, quando foo.cpp è compilato, il compilatore non può vedere bar.cpp per sapere che MyClass è necessario. Può vedere il modello MyClass , ma non può emettere un codice per questo (è un modello, non una class). E quando viene compilato bar.cpp , il compilatore può vedere che deve creare un MyClass , ma non può vedere il template MyClass (solo la sua interfaccia in foo.h ) quindi non può creare esso.

Se foo.cpp stesso usa MyClass , il codice verrà generato durante la compilazione di foo.cpp , quindi quando bar.o è collegato a foo.o possono essere collegati e funzioneranno. Possiamo usare questo fatto per consentire l’implementazione di un insieme finito di istanze di template in un file .cpp scrivendo un singolo modello. Ma non c’è modo per bar.cpp di usare il modello come modello e istanziarlo su qualsiasi tipo desideri ; può usare solo versioni preesistenti della class basata sui modelli che l’autore di foo.cpp pensava di fornire.

Si potrebbe pensare che durante la compilazione di un modello il compilatore debba “generare tutte le versioni”, con quelle che non vengono mai utilizzate filtrate durante il collegamento. A parte l’enorme sovraccarico e le estreme difficoltà che un simile approccio avrebbe dovuto affrontare perché le caratteristiche di “tipo modificatore” come puntatori e matrici permettono anche solo i tipi built-in di dare origine a un numero infinito di tipi, cosa succede quando estendo ora il mio programma aggiungendo:

  • baz.cpp
    • dichiara e implementa la class BazPrivate e utilizza MyClass

Non c’è modo che questo possa funzionare a meno che neanche noi

  1. Devo ricompilare foo.cpp ogni volta che cambiamo qualsiasi altro file nel programma , nel caso aggiungesse un nuovo romanzo di istanza di MyClass
  2. Richiede che baz.cpp contenga (possibilmente tramite header) il template completo di MyClass , in modo che il compilatore possa generare MyClass durante la compilazione di baz.cpp .

A nessuno piace (1), perché i sistemi di compilazione per l’analisi dell’intero programma richiedono sempre la compilazione e perché rendono imansible la distribuzione di librerie compilate senza il codice sorgente. Quindi abbiamo (2) invece.

I modelli devono essere istanziati dal compilatore prima di compilarli effettivamente in codice object. Questa istanziazione può essere raggiunta solo se gli argomenti del modello sono noti. Ora immagina uno scenario in cui una funzione template è dichiarata in ah , definita in a.cpp e usata in b.cpp . Quando a.cpp è compilato, non è necessariamente noto che la compilazione imminente b.cpp richiederà un’istanza del modello, figuriamoci quale specifica istanza sarebbe quella. Per ulteriori file di intestazione e di origine, la situazione può diventare rapidamente più complicata.

Si può sostenere che i compilatori possono essere resi più intelligenti per “guardare avanti” per tutti gli usi del modello, ma sono sicuro che non sarebbe difficile creare scenari ricorsivi o altrimenti complicati. AFAIK, i compilatori non fanno questo sguardo avanti. Come ha sottolineato Anton, alcuni compilatori supportano dichiarazioni di esportazione esplicite di istanze di template, ma non tutti i compilatori lo supportano (ancora?).

In realtà, le versioni dello standard C ++ prima di C ++ 11 definivano la parola chiave ‘export’, che avrebbe reso ansible dichiarare semplicemente modelli in un file di intestazione e implementarli altrove.

Sfortunatamente, nessuno dei popolari compilatori ha implementato questa parola chiave. L’unico che conosco è il frontend scritto da Edison Design Group, che viene utilizzato dal compilatore Comeau C ++. Tutti gli altri hanno insistito per scrivere modelli nei file di intestazione, necessitando della definizione del codice per una corretta istanziazione (come già sottolineato da altri).

Di conseguenza, il comitato standard ISO C ++ ha deciso di rimuovere la funzionalità di export dei modelli che iniziano con C ++ 11.

Sebbene il C ++ standard non abbia tale requisito, alcuni compilatori richiedono che tutti i modelli di funzioni e di class debbano essere resi disponibili in ogni unità di traduzione che vengono utilizzati. In effetti, per quei compilatori, i corpi delle funzioni del modello devono essere resi disponibili in un file di intestazione. Per ripetere: ciò significa che quei compilatori non permetteranno che vengano definiti in file non di intestazione come i file .cpp

Esiste una parola chiave export che dovrebbe mitigare questo problema, ma non è neanche lontanamente portatile.

I modelli devono essere utilizzati nelle intestazioni perché il compilatore deve istanziare diverse versioni del codice, in base ai parametri dati / dedotti per i parametri del modello. Ricorda che un modello non rappresenta direttamente il codice, ma un modello per diverse versioni di quel codice. Quando si compila una funzione non modello in un file .cpp , si compila una funzione / class concreta. Questo non è il caso per i modelli, che possono essere istanziati con tipi diversi, vale a dire, il codice concreto deve essere emesso quando si sostituiscono i parametri del modello con tipi concreti.

C’era una funzionalità con la parola chiave export che doveva essere utilizzata per la compilazione separata. La funzione di export è deprecata in C++11 e, AFAIK, solo un compilatore l’ha implementata. Non dovresti usare l’ export . La compilazione separata non è ansible in C++ o C++11 ma forse in C++17 , se i concetti lo fanno, potremmo avere un modo di compilazione separata.

Per ottenere una compilazione separata, deve essere ansible un controllo del corpo del modello separato. Sembra che una soluzione sia ansible con i concetti. Dai uno sguardo a questo documento recentemente presentato alla riunione del comitato standard. Penso che questo non sia l’unico requisito, dal momento che è ancora necessario creare un’istanza di codice per il codice del modello nel codice utente.

Il problema di compilazione separato per i modelli credo sia anche un problema che si presenta con la migrazione ai moduli, che è attualmente in lavorazione.

Significa che il modo più portabile per definire le implementazioni del metodo delle classi template è definirle all’interno della definizione della class template.

 template < typename ... > class MyClass { int myMethod() { // Not just declaration. Add method implementation here } }; 

Anche se ci sono molte buone spiegazioni sopra, mi manca un modo pratico per separare i template in header e body.
La mia preoccupazione principale è evitare la ricompilazione di tutti gli utenti del modello, quando cambio la sua definizione.
Avere tutte le istanze dei modelli nel corpo del modello non è una soluzione valida per me, poiché l’autore del modello potrebbe non sapere tutto se il suo utilizzo e l’utente del modello potrebbero non avere il diritto di modificarlo.
Ho seguito il seguente approccio, che funziona anche con compilatori più vecchi (gcc 4.3.4, aCC A.03.13).

Per ogni utilizzo del template c’è un typedef nel proprio file di intestazione (generato dal modello UML). Il suo corpo contiene l’istanziazione (che finisce in una biblioteca che è collegata alla fine).
Ogni utente del modello include il file di intestazione e utilizza typedef.

Un esempio schematico:

MyTemplate.h:

 #ifndef MyTemplate_h #define MyTemplate_h 1 template  class MyTemplate { public: MyTemplate(const T& rt); void dump(); T t; }; #endif 

MyTemplate.cpp:

 #include "MyTemplate.h" #include  template  MyTemplate::MyTemplate(const T& rt) : t(rt) { } template  void MyTemplate::dump() { cerr << t << endl; } 

MyInstantiatedTemplate.h:

 #ifndef MyInstantiatedTemplate_h #define MyInstantiatedTemplate_h 1 #include "MyTemplate.h" typedef MyTemplate< int > MyInstantiatedTemplate; #endif 

MyInstantiatedTemplate.cpp:

 #include "MyTemplate.cpp" template class MyTemplate< int >; 

main.cpp:

 #include "MyInstantiatedTemplate.h" int main() { MyInstantiatedTemplate m(100); m.dump(); return 0; } 

In questo modo sarà necessario ricompilare solo le istanze del modello, non tutti gli utenti del modello (e le dipendenze).

Questo è esattamente corretto perché il compilatore deve sapere che tipo è per l’allocazione. Quindi le classi template, le funzioni, le enumerazioni, ecc. Devono essere implementate anche nel file header se deve essere reso pubblico o parte di una libreria (statico o dinamico) perché i file header non sono compilati diversamente dai file c / cpp che siamo. Se il compilatore non sa che il tipo non può compilarlo. In .Net può perché tutti gli oggetti derivano dalla class Object. Questo non è .Net.

Se la preoccupazione è il tempo di compilazione e le dimensioni binarie eccessive prodotte dalla compilazione del file .h come parte di tutti i moduli .cpp che lo utilizzano, in molti casi ciò che si può fare è far discendere la class template da una class base non templata per parti dell’interfaccia non dipendenti dal tipo e quella class base può avere la sua implementazione nel file .cpp.

Un modo per avere un’implementazione separata è il seguente.

 //inner_foo.h template  struct Foo { void doSomething(T param); }; //foo.tpp #include "inner_foo.h" template  void Foo::doSomething(T param) { //implementation } //foo.h #include  //main.cpp #include  

inner_foo ha le dichiarazioni in avanti. foo.tpp ha l’implementazione e include inner_foo.h; e foo.h avrà solo una riga, per includere foo.tpp.

Al momento della compilazione, i contenuti di foo.h vengono copiati in foo.tpp e quindi l’intero file viene copiato in foo.h dopo il quale viene compilato. In questo modo, non ci sono limitazioni e la denominazione è coerente, in cambio di un file aggiuntivo.

Lo faccio perché gli analizzatori statici per il codice si interrompono quando non vedono le dichiarazioni forward di class in * .tpp. Ciò è fastidioso quando si scrive codice in qualsiasi IDE o si utilizza YouCompleteMe o altri.

Il compilatore genererà il codice per ogni istanza del modello quando si utilizza un modello durante la fase di compilazione. Nel processo di compilazione e collegamento i file .pppp vengono convertiti in codice object o macchina puro che in essi contiene riferimenti o simboli non definiti poiché i file .h che sono inclusi nel file main.cpp non hanno alcuna implementazione ANCORA. Questi sono pronti per essere collegati con un altro file object che definisce un’implementazione per il tuo modello e quindi hai un eseguibile completo a.out. Tuttavia, poiché i template devono essere elaborati nella fase di compilazione per generare codice per ogni istanza di template che si fa nel programma principale, il collegamento non aiuta perché compila main.cpp in main.o e poi compila il template .cpp in template.o e quindi il collegamento non raggiungerà lo scopo dei template perché sto collegando diverse istanze di template alla stessa implementazione del template! E i modelli dovrebbero fare l’opposto, cioè avere un’implementazione UNO ma consentire molte istanze disponibili tramite l’uso di una class.

Significato typename T get viene sostituito durante la fase di compilazione, non il passo di collegamento, quindi se provo a compilare un modello senza che T venga sostituito come un tipo di valore concreto, quindi non funzionerà perché è la definizione di modelli è un processo di compilazione e btw la meta-programmazione riguarda l’utilizzo di questa definizione.

Solo per aggiungere qualcosa degno di nota qui. Si possono definire i metodi di una class basata su modelli solo nel file di implementazione quando non sono modelli di funzione.


myQueue.hpp:

 template  class QueueA { int size; ... public: template  T dequeue() { // implementation here } bool isEmpty(); ... } 

myQueue.cpp:

 // implementation of regular methods goes like this: template  bool QueueA::isEmpty() { return this->size == 0; } main() { QueueA Q; ... }