c++ - separate - template in cpp file




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

È 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<T>
  • foo.cpp
    • definisce l'implementazione della class MyClass<T>
  • bar.cpp
    • utilizza MyClass<int>

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 collegare 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 dinamica, 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<T> non è realmente una classe 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 classe non è una classe, è una ricetta per creare una nuova classe 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<int> è necessario. Può vedere il modello MyClass<T> , ma non può emettere un codice per questo (è un modello, non una classe). E quando viene compilato bar.cpp , il compilatore può vedere che deve creare un MyClass<int> , ma non può vedere il template MyClass<T> (solo la sua interfaccia in foo.h ) quindi non può creare esso.

Se foo.cpp stesso usa MyClass<int> , 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 classe 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<BazPrivate>

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<T>
  2. Richiede che baz.cpp contenga (possibilmente tramite header) il template completo di MyClass<T> , in modo che il compilatore possa generare MyClass<BazPrivate> 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 impossibile la distribuzione di librerie compilate senza il codice sorgente. Quindi abbiamo (2) invece.

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.)


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 T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::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).


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 / classe 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 è possibile 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 possibile un controllo del corpo del modello separato. Sembra che una soluzione sia possibile con i concetti. Dai uno sguardo a questo paper presentato di recente 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.


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 oggetto 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 oggetto 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 classe.

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.


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

Ciò istanzia (e quindi rende disponibile al linker) la classe 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<int> modo da impedirne l'istanziazione in tutti gli altri file (1000?) che usano il vettore.


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 classe con l'argomento template specificato. Per esempio:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Durante la lettura di questa riga, il compilatore creerà una nuova classe (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 classe in un file di implementazione (ad esempio .tpp) e includere questo file di implementazione alla fine dell'intestazione.

// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

// Foo.tpp
template <typename T>
void Foo<T>::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 <typename T> struct Foo { ... };

//----------------------------------------    
// Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// 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 .


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 classe template da una classe base non templata per parti dell'interfaccia non dipendenti dal tipo e quella classe base può avere la sua implementazione nel file .cpp.


Sebbene il C ++ standard non abbia tale requisito, alcuni compilatori richiedono che tutti i modelli di funzioni e di classe 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.


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

myQueue.hpp:

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

    bool isEmpty();

    ...
}    

myQueue.cpp:

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


main()
{
    QueueA<char> Q;

    ...
}

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

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

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 classe in * .tpp. Ciò è fastidioso quando si scrive codice in qualsiasi IDE o si utilizza YouCompleteMe o altri.





c++-faq