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


1778

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

L'unico modo portatile per utilizzare i modelli al momento è implementarli nei file di intestazione utilizzando le funzioni incorporate.

Perchè è questo?

(Chiarimento: i file header non sono l' unica soluzione portatile. Ma sono la soluzione portatile più conveniente.)


13
Sebbene sia vero che posizionare tutte le definizioni delle funzioni del modello nel file di intestazione sia probabilmente il modo più conveniente per usarle, non è ancora chiaro cosa stia facendo "inline" in quella citazione. Non è necessario utilizzare le funzioni in linea per questo. "Inline" non ha assolutamente nulla a che fare con questo.
AnT

7
Il libro non è aggiornato.
Gerardw,

1
Un modello non è come una funzione che può essere compilata in codice byte. È solo uno schema per generare una tale funzione. Se metti un modello da solo in un file * .cpp, non c'è nulla da compilare. Inoltre, l'installazione esplicita non è in realtà un modello, ma il punto di partenza per creare una funzione dal modello che finisce nel file * .obj.
Dgrat,

5
Sono l'unico a
pensare

Risposte:


1559

Avvertenza: non lo è 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 codice non riesce è che, durante l'istanza di un modello, il compilatore crea una nuova classe con l'argomento modello 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; 

Quando legge questa riga, il compilatore creerà una nuova classe (chiamiamola FooInt), che è equivalente alla 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 quindi 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.

Soluzione alternativa

Un'altra soluzione è mantenere l'implementazione separata 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 del C ++ su questo argomento .


96
In realtà, l'istanza esplicita deve essere in un file .cpp che ha accesso alle definizioni per tutte le funzioni membro di Foo, piuttosto che nell'intestazione.
Mankarse,

11
"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" Ma perché è un'implementazione in il file .cpp non è accessibile al compilatore? Un compilatore può anche accedere alle informazioni .cpp, in che altro modo le trasformerebbe in file .obj? EDIT: la risposta a questa domanda è nel link fornito in questa risposta ...
xcrypt

31
Non credo che questo spieghi la domanda che chiaramente, la cosa chiave è ovviamente correlata alla compilation UNIT che non è menzionata in questo post
zinking il

6
@Gabson: le strutture e le classi sono equivalenti con l'eccezione che il modificatore di accesso predefinito per le classi è "privato", mentre è pubblico per le strutture. Ci sono alcune altre piccole differenze che puoi imparare guardando questa domanda .
Luc Touraille,

3
Ho aggiunto una frase all'inizio di questa risposta per chiarire che la domanda si basa su una premessa errata. Se qualcuno chiede "Perché X è vero?" quando in realtà X non è vero, dovremmo rapidamente respingere tale ipotesi.
Aaron McDaid il

250

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

Se, in fondo al file cpp di implementazione, fai un'istanza esplicita di tutti i tipi con cui verrà utilizzato il modello, il linker sarà in grado di trovarli come al solito.

Modifica: aggiunta di esempio di istanza esplicita del modello. Utilizzato dopo che il modello è stato definito e tutte le funzioni membro sono state definite.

template class vector<int>;

Ciò creerà un'istanza (e quindi renderà disponibile al linker) la classe e tutte le sue funzioni membro (solo). La sintassi simile funziona per le funzioni del modello, quindi se si hanno sovraccarichi di operatori non membri potrebbe essere necessario fare lo stesso per quelli.

L'esempio sopra è abbastanza inutile poiché il vettore è completamente definito nelle intestazioni, tranne quando un file include comune (intestazione precompilata?) Lo utilizza in extern template class vector<int>modo da impedirgli di creare un'istanza in tutti gli altri file (1000?) Che usano il vettore.


51
Ugh. Buona risposta, ma nessuna vera soluzione pulita. Elencare tutti i tipi possibili per un modello non sembra corrispondere a quello che dovrebbe essere un modello.
Jiminion

6
Questo può essere buono in molti casi, ma generalmente interrompe lo scopo del modello che ha lo scopo di consentire di utilizzare la classe con qualsiasi typesenza elencarli manualmente.
Tomáš Zato - Ripristina Monica il

7
vectornon è un buon esempio perché un contenitore è indirizzato intrinsecamente a "tutti" i tipi. Ma capita molto spesso di creare modelli pensati solo per un insieme specifico di tipi, ad esempio tipi numerici: int8_t, int16_t, int32_t, uint8_t, uint16_t, ecc. In questo caso, ha ancora senso usare un modello , ma è anche possibile creare un'istanza esplicita per l'intera serie di tipi e, a mio avviso, raccomandato.
ZioZeiv,

Utilizzato dopo che il modello è stato definito, "e tutte le funzioni membro sono state definite". Grazie !
Vitt Volt,

1
Mi sento come se mi mancasse qualcosa ... Ho inserito l'istanziazione esplicita per due tipi nel .cppfile della classe e le due istanze sono riferite da altri .cppfile, e continuo a ricevere l'errore di collegamento che i membri non sono stati trovati.
Remo

250

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

Andiamo un po 'più vicini al concreto per una spiegazione. Di 'che ho i seguenti file:

  • foo.h
    • dichiara l'interfaccia di class MyClass<T>
  • foo.cpp
    • definisce l'implementazione di class MyClass<T>
  • bar.cpp
    • usi MyClass<int>

Compilazione separata significa che dovrei essere in grado di compilare foo.cpp indipendentemente da bar.cpp . Il compilatore esegue tutto il duro lavoro di analisi, ottimizzazione e generazione di codice su ciascuna 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 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 al bar . Ho appena prodotto, senza bisogno di ricompilare foo .cpp . foo.cpp potrebbe anche essere compilato in una libreria dinamica, distribuito altrove senza foo.cpp e collegato al codice che scrivono anni dopo che ho scritto foo.cpp .

"Polimorfismo in stile istanziazione" significa che il modello MyClass<T>non è in realtà una classe generica che può essere compilata in codice che può funzionare per qualsiasi valore di T. Che dovrebbe aggiungere in testa, come la boxe, la necessità di passare puntatori a funzione di ripartitori e costruttori, ecc l'intenzione di template C ++ è quello di evitare di dover scrivere quasi identico class MyClass_int, class MyClass_floatecc, ma di essere ancora in grado di finire con il codice compilato che è per lo più come se ci fossimo 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 Tincontro. Non è possibile compilare un modello in codice, è possibile compilare solo il risultato dell'istanza del modello.

Quindi, quando viene compilato foo.cpp , il compilatore non può vedere bar.cpp per sapere che MyClass<int>è necessario. Può vedere il modello MyClass<T>, ma non può emettere codice per quello (è 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 modello MyClass<T>(solo la sua interfaccia in foo.h ) quindi non può crearlo.

Se si utilizza foo.cppMyClass<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 modello in un file .cpp scrivendo un singolo modello. Ma non c'è modo per bar.cpp di usare il modello come modello e crearne un'istanza su qualsiasi tipo gli piaccia; può usare solo versioni preesistenti della classe di modelli che l'autore di foo.cpp ha pensato di fornire.

Si potrebbe pensare che durante la compilazione di un modello il compilatore dovrebbe "generare tutte le versioni", mentre quelle che non vengono mai utilizzate vengono filtrate durante il collegamento. A parte l'enorme sovraccarico e le difficoltà estreme che un simile approccio dovrebbe affrontare perché caratteristiche del "modificatore di tipo" come puntatori e matrici consentono anche solo ai tipi incorporati di generare un numero infinito di tipi, cosa succede quando estendo ora il mio programma aggiungendo:

  • baz.cpp
    • dichiara e attua class BazPrivatee usaMyClass<BazPrivate>

Non è possibile che ciò funzioni a meno che neanche noi

  1. Devo ricompilare foo.cpp ogni volta che cambiamo qualsiasi altro file nel programma , nel caso in cui abbia aggiunto una nuova istanza diMyClass<T>
  2. È necessario che baz.cpp contenga (possibilmente tramite header include) il modello 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 di analisi dell'intero programma impiegano un'eternità a essere compilati e perché rende impossibile distribuire le librerie compilate senza il codice sorgente. Quindi abbiamo (2) invece.


50
citazione sottolineata un modello è letteralmente un modello; un modello di classe non è una classe, è una ricetta per creare una nuova classe per ogni T che incontriamo
v.oddou

Mi piacerebbe sapere, è possibile fare istanze esplicite da qualche parte diversa dall'intestazione o dal file sorgente della classe? Ad esempio, eseguili in main.cpp?
gromit190,

1
@Birger Dovresti essere in grado di farlo da qualsiasi file che abbia accesso all'implementazione completa del modello (sia perché si trova nello stesso file o tramite le intestazioni incluse).
Ben

11
@ajeh Non è retorica. La domanda è "perché devi implementare i template in un header?", Così ho spiegato le scelte tecniche che il linguaggio C ++ fa che portano a questo requisito. Prima di scrivere la mia risposta, altri hanno già fornito soluzioni alternative che non sono soluzioni complete, perché non può esserci una soluzione completa. Ho pensato che quelle risposte sarebbero state completate da una discussione più approfondita sull'angolo "perché" della domanda.
Ben

1
immaginatelo in questo modo gente ... se non steste usando i template (per codificare in modo efficiente ciò di cui avete bisogno), offrireste comunque solo alcune versioni di quella classe. quindi hai 3 opzioni. 1). non usare modelli. (come tutte le altre classi / funzioni, a nessuno importa che gli altri non possano modificare i tipi) 2). usa i modelli e documenta quali tipi possono usare. 3). dare loro l'intero bonus di implementazione (fonte) 4). date loro l'intera fonte nel caso in cui volessero creare un modello da un'altra delle vostre classi;)
Pozza

81

I modelli devono essere istanziati dal compilatore prima di compilarli effettivamente nel codice oggetto. Questa istanza può essere raggiunta solo se sono noti gli argomenti del modello. Ora immagina uno scenario in cui una funzione modello sia dichiarata a.h, definita a.cppe utilizzata in b.cpp. Quando a.cppviene compilato, non è necessariamente noto che la compilazione imminente b.cpprichiederà un'istanza del modello, per non parlare dell'istanza specifica che sarebbe. Per ulteriori file di intestazione e di origine, la situazione può rapidamente diventare 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 guardano al futuro. Come ha sottolineato Anton, alcuni compilatori supportano esplicite dichiarazioni di esportazione delle istanze dei template, ma non tutti i compilatori lo supportano (ancora?).


1
"export" è standard, ma è difficile da implementare, quindi la maggior parte dei team di compilatori non lo ha ancora fatto.
vava,

5
l'esportazione non elimina la necessità di divulgazione delle fonti, né riduce le dipendenze di compilazione, mentre richiede uno sforzo enorme da parte dei costruttori di compilatori. Quindi lo stesso Herb Sutter chiese ai costruttori di compilatori di "dimenticare" l'esportazione. Dato che il tempo necessario per investire sarebbe meglio spendere altrove ...
Pieter,

2
Quindi non credo che l'esportazione non sia implementata 'ancora'. Probabilmente non sarà mai fatto da nessun altro oltre a EDG dopo che gli altri hanno visto quanto tempo ci è voluto e quanto poco è stato guadagnato
Pieter,

3
Se questo ti interessa, il documento si chiama "Perché non possiamo permetterci l'esportazione", è elencato sul suo blog ( gotw.ca/publications ) ma nessun pdf lì (un google veloce dovrebbe alzarlo però)
Pieter

1
Ok, grazie per il buon esempio e la spiegazione. Ecco la mia domanda però: perché il compilatore non riesce a capire dove viene chiamato il modello e compila quei file prima di compilare il file di definizione? Immagino che possa essere fatto in un semplice caso ... La risposta è che le interdipendenze rovineranno l'ordine abbastanza velocemente?
Vlad,

62

In realtà, prima di C ++ 11 lo standard definiva la exportparola chiave che avrebbe reso possibile dichiarare i template in un file header e implementarli altrove.

Nessuno dei compilatori popolari ha implementato questa parola chiave. L'unico che conosco è il frontend scritto da Edison Design Group, utilizzato dal compilatore Comeau C ++. Tutti gli altri ti hanno richiesto di scrivere modelli nei file di intestazione, perché il compilatore ha bisogno della definizione del modello per un'istanza corretta (come altri hanno già sottolineato).

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


6
... e un paio di anni dopo, ho finalmente capito cosa ci exportavrebbe effettivamente dato , e cosa no ... e ora concordo con tutto il cuore delle persone EDG: non ci avrebbe portato ciò che la maggior parte delle persone (me stesso nel '11 incluso) pensa che sarebbe, e lo standard C ++ è meglio senza di esso.
DevSolar,

4
@DevSolar: questo documento è politico, ripetitivo e mal scritto. non è la solita prosa di livello standard lì. Inesorabilmente lungo e noioso, dicendo sostanzialmente 3 volte le stesse cose attraverso decine di pagine. Ma ora sono informato che l'esportazione non è esportazione. Questa è una buona informazione!
v.oddou,

1
@ v.oddou: un buon sviluppatore e un buon scrittore tecnico sono due skillset separati. Alcuni possono fare entrambe le cose, molti no. ;-)
DevSolar il

@ v.oddou Il documento non è solo scritto male, è disinformazione. Inoltre è un giro sulla realtà: quelli che sono in realtà argomenti estremamente forti per le esportazioni sono mescolati in modo da far sembrare che siano contrari all'esportazione: “scoprire numerosi buchi correlati allo ODR nello standard in presenza di esportazione. Prima dell'esportazione, il compilatore non ha dovuto diagnosticare violazioni ODR. Ora è necessario perché è necessario combinare strutture di dati interne da unità di traduzione diverse e non è possibile combinarle se in realtà rappresentano cose diverse, quindi è necessario eseguire il controllo. "
curioso

" ora devo aggiungere in quale unità di traduzione si trovava quando è successo " Duh. Quando sei costretto a usare argomenti che sono zoppi, non hai alcun argomento. Ovviamente citerai i nomi dei file nei tuoi errori, qual è il problema? Che qualcuno si innamori di quel BS è sbalorditivo. " Anche esperti come James Kanze hanno difficoltà ad accettare che l'esportazione sia davvero così. " COSA? !!!!
curioso

34

Sebbene lo standard C ++ non abbia tali requisiti, alcuni compilatori richiedono che tutti i modelli di funzioni e classi debbano essere resi disponibili in ogni unità di traduzione utilizzata. 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 consentiranno di definirli in file non di intestazione come file .cpp

Esiste una parola chiave di esportazione che dovrebbe mitigare questo problema, ma non è affatto vicina alla portabilità.


Perché non riesco a implementarli nel file .cpp con la parola chiave "inline"?
MainID

2
Puoi, e non devi nemmeno mettere "inline". Ma saresti in grado di usarli solo in quel file cpp e da nessun'altra parte.
vava,

10
Questa è quasi la risposta più accurata , tranne "ciò significa che quei compilatori non permetteranno che vengano definiti in file non header come file .cpp" è palesemente falso.
Razze di leggerezza in orbita

28

I modelli devono essere utilizzati nelle intestazioni perché il compilatore deve creare un'istanza di versioni diverse del codice, a seconda dei parametri forniti / 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 a.cpp file, si sta compilando una funzione / classe concreta. Questo non è il caso dei modelli, che possono essere istanziati con tipi diversi, vale a dire che il codice concreto deve essere emesso quando si sostituiscono i parametri modello con tipi concreti.

C'era una funzione con la exportparola chiave che doveva essere utilizzata per una compilazione separata. La exportfunzione è obsoleta C++11e, AFAIK, solo un compilatore l'ha implementata. Non dovresti farne uso export. La compilazione separata non è possibile in C++o C++11ma forse inC++17 , se i concetti entrano in gioco, potremmo avere un modo di compilazione separata.

Per ottenere una compilazione separata, deve essere possibile un controllo separato del corpo del modello. Sembra che una soluzione sia possibile con i concetti. Guarda questo documento recentemente presentato alla riunione del comitato per gli standard. Penso che questo non sia l'unico requisito, poiché è ancora necessario creare un'istanza del codice per il codice modello nel codice utente.

Il problema della compilazione separata per i modelli suppongo sia anche un problema che si sta verificando con la migrazione ai moduli, che è attualmente in fase di elaborazione.


15

Significa che il modo più portatile per definire le implementazioni dei metodi delle classi di template è definirle all'interno della definizione della classe di template.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

15

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 dei modelli, quando cambio la sua definizione.
Avere tutte le istanze del modello nel corpo del modello non è una soluzione praticabile 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 adottato il seguente approccio, che funziona anche con compilatori più vecchi (gcc 4.3.4, aCC A.03.13).

Per ogni utilizzo del modello c'è un typedef nel suo file di intestazione (generato dal modello UML). Il suo corpo contiene l'istanza (che finisce in una libreria che è collegata alla fine).
Ogni utente del modello include quel file di intestazione e usa il 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 (e le dipendenze) del modello.


1
Mi piace questo approccio ad eccezione del MyInstantiatedTemplate.hfile e del MyInstantiatedTemplatetipo aggiunto . È un po 'più pulito se non lo usi, imho. Checkout la mia risposta su una questione diversa che mostra questo: stackoverflow.com/a/41292751/4612476
Cameron Tacklind

Questo prende il meglio di due mondi. Vorrei che questa risposta fosse valutata più in alto! Vedi anche il link sopra per un'implementazione leggermente più pulita della stessa idea.
Wormer,

8

Solo per aggiungere qualcosa degno di nota qui. Si possono definire i metodi di una classe basata sul modello 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;

    ...
}

2
Per un vero uomo ??? Se questo è vero, allora la tua risposta dovrebbe essere verificata come corretta. Perché qualcuno ha bisogno di tutte quelle cose hacky voodo se puoi semplicemente definire metodi non membri del modello in .cpp?
Michele IV,

Beh, non funziona. Almeno su MSVC 2019, ottenere un simbolo esterno irrisolto per una funzione membro della classe modello.
Michele IV,

Non ho MSVC 2019 da testare. Ciò è consentito dallo standard C ++. Ora, MSVC è noto per non aver sempre aderito alle regole. Se non lo hai già fatto, prova Impostazioni progetto -> C / C ++ -> Lingua -> Modalità conformità -> Sì (permissivo-).
Nikos,

1
Questo esempio esatto funziona ma non puoi chiamare isEmptyda nessun'altra unità di traduzione oltre a myQueue.cpp...
MM

7

Se la preoccupazione è il tempo di compilazione aggiuntivo e la dimensione binaria gonfia prodotta compilando il .h come parte di tutti i moduli .cpp che lo utilizzano, in molti casi ciò che puoi fare è far scendere la classe template da una classe base non templatizzata per parti non dipendenti dal tipo di interfaccia e quella classe base può avere la sua implementazione nel file .cpp.


2
Questa risposta dovrebbe essere modificata molto di più. Ho scoperto " indipendentemente " il tuo stesso approccio e stavo cercando specificamente qualcun altro che lo avesse già usato, dal momento che sono curioso di sapere se è un modello ufficiale e se ha un nome. Il mio approccio è quello di implementare un class XBasedove ho bisogno di implementare un template class X, inserendo le parti dipendenti dal tipo Xe tutto il resto XBase.
Fabio A.

6

Questo è esattamente corretto perché il compilatore deve sapere che tipo è per l'allocazione. Quindi le classi di template, le funzioni, gli enum, ecc. Devono essere implementate anche nel file header se devono essere rese pubbliche o parte di una libreria (statica o dinamica) perché i file header NON sono compilati a differenza dei file c / cpp che siamo. Se il compilatore non sa che il tipo è non è possibile compilarlo. In .Net può perché tutti gli oggetti derivano dalla classe Object. Questo non è .Net.


5
"I file di intestazione NON sono compilati" - è un modo davvero strano di descriverlo. I file di intestazione possono far parte di un'unità di traduzione, proprio come un file "c / cpp".
Flexo

2
In realtà, è quasi l'opposto della verità, che è che i file di intestazione vengono compilati molto spesso molte volte, mentre un file di origine viene solitamente compilato una volta.
xaxxon,

6

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 .cpp vengono convertiti in puro oggetto o codice macchina che contiene riferimenti o simboli indefiniti poiché i file .h inclusi in main.cpp non hanno ANCORA un'implementazione. 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 modelli devono essere elaborati nella fase di compilazione per generare il codice per ogni istanza del modello definita, quindi la semplice compilazione di un modello separato dal suo file di intestazione non funzionerà perché vanno sempre di pari passo, proprio per la ragione ogni istanza di modello è letteralmente una nuova classe. In una classe normale è possibile separare .h e .cpp perché .h è un modello di quella classe e .cpp è l'implementazione non elaborata in modo che tutti i file di implementazione possano essere compilati e collegati regolarmente, tuttavia utilizzando i modelli .h è un modello di come la classe non dovrebbe apparire come dovrebbe apparire l'oggetto nel senso che un file .cpp modello non è un'implementazione regolare non elaborata di una classe, è semplicemente un progetto per una classe, quindi qualsiasi implementazione di un file modello .h può '

Pertanto i modelli non vengono mai compilati separatamente e vengono compilati solo laddove si abbia un'istanza concreta in qualche altro file sorgente. Tuttavia, l'istanza concreta deve conoscere l'implementazione del file modello, perché semplicemente modificando il filetypename Tl'uso di un tipo concreto nel file .h non farà il lavoro perché quello che .cpp è lì per collegare, non lo troverò più tardi perché ricordo che i modelli sono astratti e non possono essere compilati, quindi sono costretto per dare l'implementazione in questo momento in modo che io sappia cosa compilare e collegare, e ora che ho l'implementazione, questo viene collegato al file sorgente allegato. Fondamentalmente, nel momento in cui creo un'istanza di un modello, devo creare una classe completamente nuova, e non posso farlo se non so come dovrebbe apparire quella classe quando uso il tipo che fornisco a meno che non lo faccia notare al compilatore di l'implementazione del modello, quindi ora il compilatore può sostituire Tcon il mio tipo e creare una classe concreta pronta per essere compilata e collegata.

Per riassumere, i modelli sono schemi per come dovrebbero apparire le classi, le classi sono schemi per come dovrebbe apparire un oggetto. Non riesco a compilare modelli separati dalla loro istanza concreta perché il compilatore compila solo tipi concreti, in altre parole, modelli almeno in C ++, è pura astrazione del linguaggio. Dobbiamo deselezionare i modelli per così dire, e lo facciamo dando loro un tipo concreto da affrontare in modo che l'astrazione dei nostri modelli possa trasformarsi in un normale file di classe e, a sua volta, possa essere compilato normalmente. Separare il file .h modello e il file .cpp modello non ha senso. È privo di senso perché la separazione di .cpp e .h è solo dove il .cpp può essere compilato individualmente e collegato individualmente, con modelli poiché non possiamo compilarli separatamente, perché i modelli sono un'astrazione,

Il significato typename Tviene sostituito durante la fase di compilazione non nella fase di collegamento, quindi se provo a compilare un modello senza Tessere sostituito come un tipo di valore concreto che è completamente privo di significato per il compilatore e di conseguenza non è possibile creare un codice oggetto perché non lo fa sapere cos'è T.

È tecnicamente possibile creare una sorta di funzionalità che salverà il file template.cpp e cambierà i tipi quando li trova in altre fonti, penso che lo standard abbia una parola chiave exportche ti permetterà di mettere i modelli in un separato file cpp ma non che molti compilatori lo implementino.

Solo una nota a margine, quando si effettuano specializzazioni per una classe modello, è possibile separare l'intestazione dall'implementazione perché una specializzazione per definizione significa che sono specializzato per un tipo concreto che può essere compilato e collegato individualmente.


4

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 forward. foo.tpp ha l'implementazione e include inner_foo.h; e foo.h avrà solo una riga, per includere foo.tpp.

In fase di compilazione, i contenuti di foo.h vengono copiati in foo.tpp e quindi l'intero file viene copiato in foo.h dopo di che viene compilato. In questo modo, non ci sono limiti e la denominazione è coerente, in cambio di un file aggiuntivo.

Lo faccio perché gli analizzatori statici per il codice si rompono quando non vede le dichiarazioni forward della classe in * .tpp. Questo è fastidioso quando si scrive codice in qualsiasi IDE o si utilizza YouCompleteMe o altri.


2
s / inner_foo / foo / g e include foo.tpp alla fine di foo.h. Un file in meno.

1

Suggerisco di guardare questa pagina gcc che discute i compromessi tra il modello "cfront" e "borland" per le istanze dei template.

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html

Il modello "borland" corrisponde a ciò che l'autore suggerisce, fornendo la definizione completa del modello e facendo compilare le cose più volte.

Contiene raccomandazioni esplicite sull'uso della creazione di un'istanza modello manuale e automatica. Ad esempio, l'opzione "-repo" può essere utilizzata per raccogliere modelli che devono essere istanziati. Oppure un'altra opzione è disabilitare le istanze automatiche dei template usando "-fno-implicit-templates" per forzare l'istanziazione manuale dei template.

Nella mia esperienza, mi affido alla libreria standard C ++ e ai modelli Boost che vengono istanziati per ogni unità di compilazione (usando una libreria di modelli). Per le mie classi di template di grandi dimensioni, eseguo un'istanza di template manuale, una volta, per i tipi di cui ho bisogno.

Questo è il mio approccio perché sto fornendo un programma di lavoro, non una libreria di modelli da utilizzare in altri programmi. L'autore del libro, Josuttis, lavora molto sulle librerie di modelli.

Se fossi davvero preoccupato per la velocità, suppongo che esplorerei usando le intestazioni precompilate https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

che sta guadagnando supporto in molti compilatori. Tuttavia, penso che le intestazioni precompilate sarebbero difficili con i file di intestazione del modello.


-2

Un altro motivo per cui è una buona idea scrivere dichiarazioni e definizioni nei file di intestazione è per la leggibilità. Supponiamo che ci sia una tale funzione modello in Utility.h:

template <class T>
T min(T const& one, T const& theOther);

E in Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

Ciò richiede che tutte le classi T qui implementino l'operatore minore di (<). Viene generato un errore del compilatore quando si confrontano due istanze di classe che non hanno implementato "<".

Pertanto, se separi la dichiarazione e la definizione del modello, non sarai in grado di leggere solo il file di intestazione per vedere i dettagli di questo modello al fine di utilizzare questa API nelle tue classi, anche se il compilatore ti dirà in questo caso su quale operatore deve essere ignorato.


-7

Puoi effettivamente definire la tua classe di template all'interno di un file .template anziché di un file .cpp. Chiunque dica che puoi definirlo solo all'interno di un file di intestazione ha torto. Questo è qualcosa che risale al c ++ 98.

Non dimenticare di fare in modo che il tuo compilatore tratti il ​​tuo file .template come un file c ++ per mantenere il senso di Intelli.

Ecco un esempio di questo per una classe di array dinamica.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Ora all'interno del tuo file .template definisci le tue funzioni come faresti normalmente.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }

2
La maggior parte delle persone definirebbe un file di intestazione come qualsiasi cosa che propaga le definizioni ai file di origine. Quindi potresti aver deciso di utilizzare l'estensione ".template" ma hai scritto un file di intestazione.
Tommy,
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.