Memorizzazione delle definizioni delle funzioni del modello C ++ in un file .CPP


526

Ho un codice modello che preferirei archiviare in un file CPP anziché inline nell'intestazione. So che questo può essere fatto fintanto che sai quali tipi di template verranno utilizzati. Per esempio:

.h file

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

file .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Nota le ultime due righe: la funzione template foo :: do viene utilizzata solo con ints e std :: stringhe, quindi queste definizioni indicano che l'app si collegherà.

La mia domanda è: è un brutto trucco o funzionerà con altri compilatori / linker? Al momento sto solo usando questo codice con VS2008, ma vorrò portarlo su altri ambienti.


22
Non avevo idea che ciò fosse possibile: un trucco interessante! Avrebbe aiutato alcuni compiti recenti considerevoli a saperlo - evviva!
xan,

69
La cosa che mi colpisce è l'uso di docome identificatore: p
Quentin

ho fatto qualcosa di simile con gcc, ma sto ancora facendo delle ricerche
Nick,

16
Questo non è un "hack", è una declerazione diretta. Questo ha un posto nello standard della lingua; quindi sì, è consentito in ogni compilatore conforme standard.
Ahmet Ipkin

1
E se hai dozzine di metodi? Puoi farlo template class foo<int>;template class foo<std::string>;alla fine del file .cpp?
Ignorante l'

Risposte:


231

Il problema che descrivi può essere risolto definendo il modello nell'intestazione o tramite l'approccio descritto sopra.

Consiglio di leggere i seguenti punti dalla FAQ Lite di C ++ :

Entrano in molti dettagli su questi (e altri) problemi del modello.


39
Solo per integrare la risposta, il link di riferimento risponde positivamente alla domanda, ovvero è possibile fare ciò che Rob ha suggerito e avere il codice per essere portatile.
Ivotron

161
Puoi semplicemente pubblicare le parti pertinenti nella risposta stessa? Perché tale riferimento è consentito anche su SO. Non ho idea di cosa cercare in questo link in quanto è stato fortemente modificato da allora.
Ident

124

Per gli altri in questa pagina che si chiedono quale sia la sintassi corretta (come ho fatto io) per la specializzazione esplicita del modello (o almeno in VS2008), è il seguente ...

Nel tuo file .h ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

E nel tuo file .cpp

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

15
Intendi "per la specializzazione esplicita del modello CLASS". In quel caso coprirà ogni funzione che ha la classe di modello?
Arthur,

@Arthur non sembra, ho alcuni metodi di template che rimangono nell'intestazione e la maggior parte degli altri metodi in cpp, funziona bene. Soluzione molto bella.
user1633272,

Nel caso del richiedente, hanno un modello di funzione, non un modello di classe.
user253751

23

Questo codice è ben formato. Devi solo prestare attenzione al fatto che la definizione del modello è visibile nel punto di istanza. Per citare lo standard, § 14.7.2.4:

La definizione di un modello di funzione non esportato, un modello di funzione membro non esportato o una funzione membro non esportata o un membro dati statico di un modello di classe deve essere presente in ogni unità di traduzione in cui è esplicitamente istanziato.


2
Che cosa significa non esportato ?
Dan Nissenbaum,

1
@Dan Visibile solo all'interno della sua unità di compilazione, non al di fuori di essa. Se si collegano più unità di compilazione insieme, è possibile utilizzare simboli esportati su di esse (e devono avere una singola, o almeno, nel caso di modelli, definizioni coerenti, altrimenti ci si imbatte in UB).
Konrad Rudolph,

Grazie. Ho pensato che tutte le funzioni sono (per impostazione predefinita) visibili all'esterno dell'unità di compilazione. Se ho due unità di compilazione a.cpp(che definiscono la funzione a() {}) e b.cpp(che definiscono la funzione b() { a() }), questo si collegherà correttamente. Se ho ragione, la citazione di cui sopra sembrerebbe non valere per il caso tipico ... sto sbagliando da qualche parte?
Dan Nissenbaum,

@Dan Trivial controesempio: inlinefunzioni
Konrad Rudolph,

1
I modelli di funzione @Dan sono implicitamente inline. Il motivo è che senza un ABI C ++ standardizzato è difficile / impossibile definire l'effetto che altrimenti avrebbe.
Konrad Rudolph,

15

Questo dovrebbe funzionare bene ovunque siano supportati i modelli. L'istanza esplicita del modello fa parte dello standard C ++.


13

Il tuo esempio è corretto ma non molto portatile. C'è anche una sintassi leggermente più pulita che può essere usata (come sottolineato da @ namespace-sid).

Supponiamo che la classe basata sul modello faccia parte di alcune librerie da condividere. Dovrebbero essere compilate altre versioni della classe modello? Il manutentore della biblioteca dovrebbe anticipare tutti gli usi possibili della classe?

Un approccio alternativo è una leggera variazione su ciò che hai: aggiungi un terzo file che è il file di implementazione / istanza del modello.

file foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

file foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

file foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

L'unico avvertimento è che devi dire al compilatore di compilare foo-impl.cppinvece foo.cppche compilare quest'ultimo non fa nulla.

Naturalmente, puoi avere più implementazioni nel terzo file o avere più file di implementazione per ogni tipo che desideri utilizzare.

Ciò consente molta più flessibilità quando si condivide la classe basata su modelli per altri usi.

Questa impostazione riduce anche i tempi di compilazione per le classi riutilizzate perché non stai ricompilando lo stesso file di intestazione in ogni unità di traduzione.


cosa ti compra questo? È ancora necessario modificare foo-impl.cpp per aggiungere una nuova specializzazione.
MK.

Separazione dei dettagli di implementazione (aka definizioni in foo.cpp) da cui le versioni sono in realtà compilati (in foo-impl.cpp) e dichiarazioni (in foo.h). Non mi piace il fatto che la maggior parte dei modelli C ++ sia definita interamente in file header. Questo è in contrasto con lo standard C / C ++ di coppie c[pp]/hper ogni classe / spazio dei nomi / qualunque raggruppamento si usi. Le persone sembrano ancora utilizzare file di intestazione monolitici semplicemente perché questa alternativa non è ampiamente utilizzata o conosciuta.
Cameron Tacklind,

1
@MK. In un primo momento stavo inserendo le istanze esplicite del modello alla fine della definizione nel file sorgente fino a quando non avevo bisogno di ulteriori istanze altrove (ad esempio test unitari usando un modello simulato come tipo di modello). Questa separazione mi consente di aggiungere più istanze esternamente. Inoltre, funziona ancora quando ho mantenere l'originale come una h/cppcoppia anche se ho dovuto circondare l'elenco originale di istanze in un include guardia, ma potrebbe ancora compilare il foo.cppcome di consueto. Sono ancora abbastanza nuovo in C ++ e sarei interessato a sapere se questo uso misto ha qualche avvertimento aggiuntivo.
Terza acqua,

3
Penso che sia preferibile disaccoppiare foo.cppe foo-impl.cpp. Non #include "foo.cpp"nel foo-impl.cppfile; invece, aggiungere la dichiarazione extern template class foo<int>;a foo.cppper impedire al compilatore di creare un'istanza del modello durante la compilazione foo.cpp. Assicurarsi che il sistema di compilazione costruisca entrambi i .cppfile e passi entrambi i file oggetto al linker. Questo ha molteplici vantaggi: a) è chiaro in foo.cppquanto non vi è alcuna istanza; b) le modifiche a foo.cpp non richiedono una ricompilazione di foo-impl.cpp.
Shmuel Levine,

3
Questo è un ottimo approccio al problema delle definizioni dei modelli che prende il meglio di entrambi i mondi: implementazione dell'intestazione e istanziazione per i tipi usati di frequente. L'unica modifica che vorrei apportare a questa configurazione è quella di rinominare foo.cppin foo_impl.he foo-impl.cppin just foo.cpp. Vorrei anche aggiungere typedef per le istanze da foo.cppa foo.h, allo stesso modo using foo_int = foo<int>;. Il trucco è fornire agli utenti due interfacce di intestazione per una scelta. Quando l'utente ha bisogno di un'istanza predefinita, include foo.h, quando l'utente ha bisogno di qualcosa che non funziona, include foo_impl.h.
Wormer,

5

Questo non è sicuramente un brutto trucco, ma fai attenzione al fatto che dovrai farlo (la specializzazione esplicita del modello) per ogni classe / tipo che vuoi usare con il modello dato. In caso di MOLTI tipi che richiedono l'istanza del modello, nel file .cpp possono essere presenti MOLTE righe. Per risolvere questo problema puoi avere un TemplateClassInst.cpp in ogni progetto che usi in modo da avere un maggiore controllo su quali tipi verranno istanziati. Ovviamente questa soluzione non sarà perfetta (alias proiettile d'argento) in quanto potresti finire per rompere l'ODR :).


Sei sicuro che romperà l'ODR? Se le righe di istanza in TemplateClassInst.cpp fanno riferimento al file di origine identico (contenente le definizioni delle funzioni del modello), non è garantito che non violi l'ODR poiché tutte le definizioni sono identiche (anche se ripetute)?
Dan Nissenbaum,

Per favore, cos'è ODR?
inamovibile il

4

Nell'ultimo standard esiste una parola chiave ( export) che potrebbe aiutare ad alleviare questo problema, ma non è implementata in nessun compilatore di cui io sia a conoscenza, tranne Comeau.

Vedi le FAQ-lite su questo.


2
AFAIK, l'esportazione è morta perché si trovano ad affrontare problemi sempre nuovi, ogni volta che risolvono l'ultimo, rendendo la soluzione globale sempre più complicata. E la parola chiave "export" non ti consentirà comunque di "esportare" da un CPP (sempre da H. Sutter). Quindi dico: non trattenere il respiro ...
Paercebal,

2
Per implementare l'esportazione, il compilatore richiede ancora la definizione completa del modello. Tutto ciò che guadagni è averlo in una sorta di forma compilata. Ma davvero non ha senso.
Zan Lynx,

2
... ed è passato dallo standard, a causa di complicazioni eccessive per un guadagno minimo.
DevSolar,

4

Questo è un modo standard per definire le funzioni del modello. Penso che ci siano tre metodi che ho letto per definire i template. O probabilmente 4. Ognuno con pro e contro.

  1. Definire nella definizione della classe. Non mi piace affatto perché penso che le definizioni di classe siano strettamente di riferimento e dovrebbero essere facili da leggere. Tuttavia, è molto meno complicato definire i modelli in classe che all'esterno. E non tutte le dichiarazioni dei modelli sono allo stesso livello di complessità. Questo metodo rende anche il modello un modello vero.

  2. Definire il modello nella stessa intestazione, ma al di fuori della classe. Questo è il mio modo preferito la maggior parte delle volte. Mantiene in ordine la definizione della tua classe, il modello rimane un modello vero. Richiede tuttavia la denominazione completa del modello che può essere complicata. Inoltre, il tuo codice è disponibile per tutti. Ma se hai bisogno che il tuo codice sia in linea, questo è l'unico modo. Puoi anche farlo creando un file .INL alla fine delle definizioni di classe.

  3. Includi header.h e deployment.CPP nel tuo main.CPP. Penso che sia così. Non dovrai preparare alcuna pre-istanza, si comporterà come un vero modello. Il problema che ho con esso è che non è naturale. Normalmente non includiamo e prevediamo di includere i file di origine. Immagino che, dal momento che hai incluso il file sorgente, le funzioni del modello possano essere integrate.

  4. Quest'ultimo metodo, che era il modo pubblicato, sta definendo i template in un file sorgente, proprio come il numero 3; ma invece di includere il file sorgente, pre-istanziamo i modelli a quelli di cui avremo bisogno. Non ho problemi con questo metodo ed è utile a volte. Abbiamo un grosso codice, non può trarre vantaggio dall'essere inline quindi inseriscilo in un file CPP. E se conosciamo le istanze comuni e possiamo predefinirle. Questo ci salva dallo scrivere praticamente la stessa cosa 5, 10 volte. Questo metodo ha il vantaggio di mantenere il nostro codice proprietario. Ma non consiglio di inserire minuscole funzioni regolarmente utilizzate nei file CPP. Poiché ciò ridurrà le prestazioni della tua libreria.

Nota, non sono a conoscenza delle conseguenze di un file obj gonfio.


3

Sì, questo è il modo standard per creare istanze esplicite di specializzazione . Come hai affermato, non puoi creare un'istanza di questo modello con altri tipi.

Modifica: corretto in base al commento.


Essere pignoli sulla terminologia è una "istanza esplicita".
Richard Corden,

2

Facciamo un esempio, diciamo per qualche motivo che vuoi avere una classe template:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Se compili questo codice con Visual Studio, funziona immediatamente. gcc produrrà un errore del linker (se lo stesso file di intestazione viene utilizzato da più file .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

È possibile spostare l'implementazione nel file .cpp, ma è necessario dichiarare la classe in questo modo -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

E poi .cpp avrà questo aspetto:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Senza le ultime due righe nel file di intestazione - gcc funzionerà bene, ma Visual Studio produrrà un errore:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

La sintassi della classe template è facoltativa nel caso in cui si desideri esporre la funzione tramite l'esportazione .dll, ma questo è applicabile solo per la piattaforma Windows - quindi test_template.h potrebbe apparire così:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

con il file .cpp dell'esempio precedente.

Questo, tuttavia, dà più mal di testa al linker, quindi si consiglia di utilizzare l'esempio precedente se non si esporta la funzione .dll.


1

Tempo per un aggiornamento! Crea un file inline (.inl, o probabilmente qualsiasi altro) e copia semplicemente tutte le tue definizioni in esso. Assicurati di aggiungere il modello sopra ogni funzione ( template <typename T, ...>). Ora invece di includere il file header nel file inline fai il contrario. Includi il file inline dopo la dichiarazione della tua classe ( #include "file.inl").

Non so davvero perché nessuno lo abbia menzionato. Non vedo svantaggi immediati.


25
Gli svantaggi immediati sono fondamentalmente gli stessi della definizione delle funzioni del modello direttamente nell'intestazione. Una volta tu #include "file.inl", il preprocessore incollerà il contenuto di file.inldirettamente nell'intestazione. Qualunque sia la ragione per cui si desidera evitare l'implementazione nell'intestazione, questa soluzione non risolve questo problema.
Cody Grey

5
- e significa che, tecnicamente inutilmente, ti stai caricando del compito di scrivere tutto il prolisso e sbalorditivo scaldabagno necessario per le templatedefinizioni fuori linea . Capisco perché le persone vogliono farlo - per ottenere la massima parità con dichiarazioni / definizioni non modello, mantenere ordinata la dichiarazione dell'interfaccia, ecc. - Ma non sempre vale la seccatura. Si tratta di valutare i compromessi su entrambi i lati e scegliere il meno negativo . ... fino a namespace classdiventare una cosa: O [per favore, sii una cosa ]
underscore_d

2
@Andrew Sembra che sia rimasto bloccato nelle pipe del Comitato, anche se penso di aver visto qualcuno dire che non era intenzionale. Vorrei che fosse diventato C ++ 17. Forse il prossimo decennio.
underscore_d

@CodyGray: tecnicamente, questo è davvero lo stesso per il compilatore e quindi non riduce il tempo di compilazione. Penso comunque che valga la pena menzionarlo e metterlo in pratica in numerosi progetti che ho visto. Percorrere questa strada aiuta a separare l'interfaccia dalla definizione, che è una buona pratica. In questo caso non aiuta con la compatibilità ABI o simili, ma facilita la lettura e la comprensione dell'interfaccia.
chiloalphaindia

0

Non c'è niente di sbagliato nell'esempio che hai dato. Ma devo dire che credo che non sia efficiente memorizzare le definizioni delle funzioni in un file cpp. Comprendo solo la necessità di separare la dichiarazione e la definizione della funzione.

Se utilizzato insieme all'istanza esplicita della classe, la libreria di controllo del concetto Boost (BCCL) può aiutarti a generare il codice funzione modello nei file cpp.


8
Cosa c'è di inefficiente al riguardo?
Cody Grey

0

Nessuno dei precedenti ha funzionato per me, quindi ecco come l'hai risolto, la mia classe ha solo un metodo modellato ..

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

questo evita errori del linker e non è necessario chiamare TemporaryFunction

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.