Por que os modelos só podem ser implementados no arquivo de header?

Citação da biblioteca padrão C ++: um tutorial e um manual :

A única maneira portátil de usar modelos no momento é implementá-los em arquivos de header usando funções embutidas.

Por que é isso?

(Esclarecimento: os arquivos de header não são a única solução portátil. Mas são a solução portátil mais conveniente.)

Não é necessário colocar a implementação no arquivo de header, veja a solução alternativa no final desta resposta.

De qualquer forma, o motivo pelo qual seu código está falhando é que, ao instanciar um modelo, o compilador cria uma nova class com o argumento de modelo fornecido. Por exemplo:

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

Ao ler esta linha, o compilador irá criar uma nova class (vamos chamá-lo FooInt ), que é equivalente ao seguinte:

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

Consequentemente, o compilador precisa ter access à implementação dos methods, para instanciá-los com o argumento template (neste caso, int ). Se essas implementações não estivessem no header, elas não seriam acessíveis e, portanto, o compilador não seria capaz de instanciar o modelo.

Uma solução comum para isso é gravar a declaração de modelo em um arquivo de header e, em seguida, implementar a class em um arquivo de implementação (por exemplo, .tpp) e include esse arquivo de implementação no final do header.

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

Dessa forma, a implementação ainda é separada da declaração, mas é acessível ao compilador.

Outra solução é manter a implementação separada e instanciar explicitamente todas as instâncias de modelo necessárias:

 // 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 a minha explicação não for clara o suficiente, você pode dar uma olhada no C ++ Super-FAQ sobre este assunto .

Muitas respostas corretas aqui, mas eu queria adicionar isso (por completude):

Se você, na parte inferior do arquivo cpp de implementação, fizer instanciação explícita de todos os tipos com os quais o modelo será usado, o vinculador poderá encontrá-los normalmente.

Editar: Adicionando exemplo de instanciação de modelo explícita. Usado após o modelo ter sido definido e todas as funções de membro terem sido definidas.

 template class vector; 

Isso irá instanciar (e, portanto, disponibilizar para o vinculador) a class e todas as suas funções de membro (somente). A syntax semelhante funciona para funções de modelo, portanto, se você tiver sobrecargas de operador que não sejam de membro, talvez seja necessário fazer o mesmo para elas.

O exemplo acima é bastante inútil, pois o vetor é totalmente definido em headers, exceto quando um arquivo de inclusão comum (header pré-compilado?) Usa o extern template class vector para evitar que instancie-o em todos os outros arquivos (1000?) que usam vetor.

É por causa do requisito de compilation separada e porque os modelos são polymorphism de estilo de instanciação.

Vamos ficar um pouco mais perto do concreto para uma explicação. Digamos que eu tenha os seguintes arquivos:

  • foo.h
    • declara a interface da class MyClass
  • foo.cpp
    • define a implementação da class MyClass
  • bar.cpp
    • usa MyClass

Compilação separada significa que eu deveria ser capaz de compilar o foo.cpp independentemente do bar.cpp . O compilador faz todo o trabalho duro de análise, otimização e geração de código em cada unidade de compilation de forma totalmente independente; não precisamos fazer análises de todo o programa. É apenas o vinculador que precisa lidar com todo o programa de uma só vez, e o trabalho do vinculador é substancialmente mais fácil.

bar.cpp nem precisa existir quando eu compilo o foo.cpp , mas eu ainda devo ser capaz de linkar o foo.o que eu já tinha junto com o bar.o eu acabei de produzir, sem precisar recompilar o foo .cpp O foo.cpp pode até mesmo ser compilado em uma biblioteca dinâmica, distribuído em outro local sem o foo.cpp e vinculado ao código que eles escrevem anos depois de eu escrever foo.cpp .

“Polimorfismo no estilo de instanciação” significa que o modelo MyClass não é realmente uma class genérica que pode ser compilada em código que pode funcionar para qualquer valor de T Isso aumentaria a sobrecarga, como o boxe, a necessidade de passar pointers de function para alocadores e construtores, etc. A intenção dos modelos C ++ é evitar escrever class MyClass_int quase idêntica class MyClass_int , class MyClass_float , etc, mas ainda assim poder terminar com código compilado que é principalmente como se tivéssemos escrito cada versão separadamente. Então, um modelo é literalmente um modelo; Um modelo de class não é uma class, é uma receita para criar uma nova class para cada T que encontramos. Um template não pode ser compilado em código, apenas o resultado de instanciar o template pode ser compilado.

Então, quando foo.cpp é compilado, o compilador não pode ver bar.cpp para saber que MyClass é necessário. Ele pode ver o modelo MyClass , mas não pode emitir código para isso (é um modelo, não uma class). E quando o bar.cpp é compilado, o compilador pode ver que ele precisa criar um MyClass , mas ele não pode ver o modelo MyClass (apenas sua interface em foo.h ) para que ele não possa criar isto.

Se foo.cpp em si usa MyClass , então o código para isso será gerado durante a compilation de foo.cpp , então quando bar.o estiver ligado a foo.o eles podem ser ligados e funcionarão. Podemos usar esse fato para permitir que um conjunto finito de instanciações de modelo seja implementado em um arquivo .cpp, escrevendo um único modelo. Mas não há como o bar.cpp usar o modelo como um modelo e instanciá-lo em qualquer tipo que desejar; ele só pode usar versões pré-existentes da class modelo que o autor do foo.cpp pensou em fornecer.

Você pode pensar que, ao compilar um modelo, o compilador deve “gerar todas as versões”, com as que nunca são usadas filtradas durante a vinculação. Além da enorme sobrecarga e das dificuldades extremas que tal abordagem enfrentaria porque os resources de “modificador de tipo” como pointers e matrizes permitem que até mesmo os tipos internos gerem um número infinito de tipos, o que acontece quando eu estendo meu programa agora adicionando:

  • baz.cpp
    • declara e implementa a class BazPrivate e usa MyClass

Não há maneira possível de que isso funcione a menos que nós

  1. Tem que recompilar o foo.cpp sempre que alteramos qualquer outro arquivo no programa , caso tenha adicionado uma nova instanciação do MyClass
  2. Exija que o baz.cpp contenha (possivelmente através do header inclui) o template completo do MyClass , para que o compilador possa gerar MyClass durante a compilation do baz.cpp .

Ninguém gosta de (1), porque os sistemas de compilation de análise de todo o programa levam uma eternidade para compilar e porque isso torna impossível distribuir bibliotecas compiladas sem o código fonte. Então nós temos (2) em vez disso.

Modelos precisam ser instanciados pelo compilador antes de compilá-los em código de object. Essa instanciação só pode ser obtida se os argumentos do modelo forem conhecidos. Agora imagine um cenário em que uma function de modelo é declarada em ah , definida em a.cpp e usada em b.cpp . Quando a.cpp é compilado, não é necessariamente sabido que a próxima compilation b.cpp exigirá uma instância do modelo, sem falar na instância específica que seria. Para mais arquivos de header e fonte, a situação pode ficar mais complicada rapidamente.

Pode-se argumentar que os compiladores podem ser mais inteligentes para “olhar para frente” para todos os usos do modelo, mas tenho certeza de que não seria difícil criar cenários recursivos ou complicados. AFAIK, os compiladores não fazem esse tipo de look ahead. Como Anton apontou, alguns compiladores suportam declarações de exportação explícitas de instanciações de modelos, mas nem todos os compiladores suportam (ainda?).

Na verdade, versões do padrão C ++ antes do C ++ 11 definiam a palavra-chave ‘export’, o que tornaria possível simplesmente declarar modelos em um arquivo de header e implementá-los em outro lugar.

Infelizmente, nenhum dos compiladores populares implementou essa palavra-chave. O único que eu conheço é o frontend escrito pelo Edison Design Group, que é usado pelo compilador Comeau C ++. Todos os outros insistiram para que você escrevesse modelos em arquivos de header, precisando da definição do código para instanciação adequada (como outros já apontaram).

Como resultado, o comitê do padrão ISO C ++ decidiu remover o recurso de export de modelos começando com C ++ 11.

Embora o C ++ padrão não possua tal requisito, alguns compiladores exigem que todos os modelos de function e de class precisem ser disponibilizados em todas as unidades de tradução utilizadas. Na verdade, para esses compiladores, os corpos das funções de modelo devem ser disponibilizados em um arquivo de header. Para repetir: isso significa que esses compiladores não permitirão que eles sejam definidos em arquivos que não sejam de header, como arquivos .cpp.

Há uma palavra-chave de exportação que supostamente atenua esse problema, mas está longe de ser portátil.

Modelos devem ser usados ​​em headers porque o compilador precisa instanciar diferentes versões do código, dependendo dos parâmetros dados / deduzidos para os parâmetros do modelo. Lembre-se de que um modelo não representa o código diretamente, mas um modelo para várias versões desse código. Quando você compila uma function não modelo em um arquivo .cpp , você está compilando uma function / class concreta. Este não é o caso dos modelos, que podem ser instanciados com diferentes tipos, ou seja, o código concreto deve ser emitido ao replace os parâmetros do modelo por tipos concretos.

Havia um recurso com a palavra-chave de export que deveria ser usada para compilation separada. O recurso de export está obsoleto no C++11 e, no AFAIK, apenas um compilador o implementou. Você não deve fazer uso de export . A compilation separada não é possível em C++ ou em C++11 mas talvez em C++17 , se os conceitos forem implementados, poderemos ter uma forma de compilation separada.

Para compilation separada a ser obtida, a verificação separada do corpo do modelo deve ser possível. Parece que uma solução é possível com conceitos. Dê uma olhada neste documento recentemente apresentado na reunião do comitê de padrões. Acho que esse não é o único requisito, pois você ainda precisa instanciar o código para o código do modelo no código do usuário.

O problema de compilation separado para modelos Eu acho que também é um problema que está surgindo com a migration para módulos, que está sendo trabalhada atualmente.

Isso significa que a maneira mais portátil de definir implementações de método de classs de modelo é defini-las dentro da definição de class de modelo.

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

Embora haja muitas boas explicações acima, estou perdendo uma maneira prática de separar modelos em header e corpo.
Minha principal preocupação é evitar a recompilation de todos os usuários do template, quando mudo sua definição.
Ter todas as instanciações de modelo no corpo do modelo não é uma solução viável para mim, uma vez que o autor do modelo pode não saber tudo se seu uso e o usuário do modelo não tiverem o direito de modificá-lo.
Tomei a seguinte abordagem, que também funciona para compiladores mais antigos (gcc 4.3.4, aCC A.03.13).

Para cada uso de modelo, há um typedef em seu próprio arquivo de header (gerado a partir do modelo UML). Seu corpo contém a instanciação (que acaba em uma biblioteca que está ligada no final).
Cada usuário do modelo inclui esse arquivo de header e usa o typedef.

Um exemplo esquemático:

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

Dessa forma, somente as instanciações de modelo precisarão ser recompiladas, nem todos os usuários de modelo (e dependencies).

Isso é exatamente correto porque o compilador precisa saber que tipo é para alocação. Portanto, classs de modelo, funções, enums, etc. devem ser implementadas também no arquivo de header se ele for tornado público ou parte de uma biblioteca (estática ou dinâmica) porque os arquivos de header NÃO são compilados, ao contrário dos arquivos c / cpp que estamos. Se o compilador não souber o tipo, não será possível compilá-lo. Em .Net pode porque todos os objects derivam da class Object. Isso não é .Net.

Se a preocupação é o tempo de compilation extra eo tamanho do binário produzido pela compilation do .h como parte de todos os módulos .cpp que o usam, em muitos casos o que você pode fazer é fazer com que a class template saia de uma class base não-modelada para partes não dependentes do tipo da interface, e essa class base pode ter sua implementação no arquivo .cpp.

Uma maneira de ter implementação separada é a seguinte.

 //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 tem as declarações futuras. foo.tpp tem a implementação e inclui inner_foo.h; e foo.h terá apenas uma linha, para include foo.tpp.

Em tempo de compilation, o conteúdo de foo.h é copiado para foo.tpp e, em seguida, o arquivo inteiro é copiado para foo.h após o qual é compilado. Desta forma, não há limitações, e a nomenclatura é consistente, em troca de um arquivo extra.

Eu faço isso porque os analisadores estáticos para o código quebram quando ele não vê as declarações de encaminhamento de class em * .tpp. Isso é irritante ao escrever código em qualquer IDE ou usando YouCompleteMe ou outros.

O compilador gerará código para cada instanciação de modelo quando você usar um modelo durante a etapa de compilation. No processo de compilation e vinculação, os arquivos .cpp são convertidos em object puro ou código de máquina, o que neles contém referências ou símbolos indefinidos, porque os arquivos .h que estão incluídos no main.cpp não têm nenhuma implementação YET. Eles estão prontos para serem vinculados a outro arquivo de object que define uma implementação para seu modelo e, portanto, você tem um executável completo a.out. No entanto, como os modelos precisam ser processados ​​na etapa de compilation para gerar código para cada instanciação de modelo que você faz em seu programa principal, vincular não ajudará porque compilar o main.cpp em main.o e, em seguida, compilar seu modelo .cpp em template.o e, em seguida, vinculação não vai conseguir o propósito de modelos porque eu estou ligando instanciação de modelo diferente para a implementação do mesmo modelo! E os modelos devem fazer o oposto, ou seja, ter UMA implementação, mas permitir muitas instanciações disponíveis através do uso de uma class.

Significado typename T get’s substituído durante a etapa de compilation não a etapa de vinculação então se eu tentar compilar um template sem T sendo substituído como um tipo de valor concreto então não funcionará porque essa é a definição de templates é um processo de tempo de compilation, e meta-programação é toda sobre o uso desta definição.

Apenas para adicionar algo digno de nota aqui. Pode-se definir methods de uma class modelada muito bem no arquivo de implementação quando eles não são modelos de function.


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