Citação da biblioteca padrão C ++: um tutorial e manual :
A única maneira portátil de usar modelos no momento é implementá-los em arquivos de cabeçalho usando funções embutidas.
Por que é isso?
(Esclarecimento: os arquivos de cabeçalho não são a única solução portátil. Mas são a solução portátil mais conveniente.)
Respostas:
Ressalva: É não necessário colocar a implementação no cabeçalho do arquivo, consulte 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 classe com o argumento de modelo fornecido. Por exemplo:
Ao ler esta linha, o compilador criará uma nova classe (vamos chamá-la
FooInt
), que é equivalente ao seguinte:Conseqüentemente, o compilador precisa ter acesso à implementação dos métodos, para instancia-los com o argumento do modelo (neste caso
int
). Se essas implementações não estivessem no cabeçalho, elas não seriam acessíveis e, portanto, o compilador não seria capaz de instanciar o modelo.Uma solução comum para isso é escrever a declaração do modelo em um arquivo de cabeçalho, implementar a classe em um arquivo de implementação (por exemplo .tpp) e incluir esse arquivo de implementação no final do cabeçalho.
Foo.h
Foo.tpp
Dessa forma, a implementação ainda é separada da declaração, mas é acessível ao compilador.
Solução alternativa
Outra solução é manter a implementação separada e instanciar explicitamente todas as instâncias de modelo necessárias:
Foo.h
Foo.cpp
Se minha explicação não for clara o suficiente, você pode dar uma olhada na Super FAQ do C ++ sobre este assunto .
fonte
Muitas respostas corretas aqui, mas eu queria acrescentar isso (para ser completo):
Se você, na parte inferior do arquivo cpp de implementação, executar 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 explícita de modelo. Usado depois que o modelo foi definido e todas as funções de membro foram definidas.
Isso instanciará (e, assim, disponibilizará ao vinculador) a classe e todas as suas funções-membro (apenas). Sintaxe semelhante funciona para funções de modelo; portanto, se você tiver sobrecargas de operador que não sejam membros, poderá ser necessário fazer o mesmo para elas.
O exemplo acima é bastante inútil, pois o vetor é totalmente definido nos cabeçalhos, exceto quando um arquivo de inclusão comum (cabeçalho pré-compilado?) É usado
extern template class vector<int>
para impedir que ele seja instanciado em todos os outros arquivos (1000?) Que usam o vetor.fonte
type
sem listá-las manualmente.vector
não é um bom exemplo porque um contêiner está segmentando inerentemente "todos" os tipos. Mas acontece com muita freqüência que você cria modelos destinados apenas a um conjunto específico de tipos, por exemplo, tipos numéricos: int8_t, int16_t, int32_t, uint8_t, uint16_t etc. Nesse caso, ainda faz sentido usar um modelo , mas instancia-los explicitamente para todo o conjunto de tipos também é possível e, na minha opinião, recomendado..cpp
arquivo da classe e as duas instanciações são mencionadas em outros.cpp
arquivos, e ainda recebo o erro de vinculação de que os membros não foram encontrados.É por causa do requisito de compilação separada e porque os modelos são polimorfismos no estilo de instanciação.
Vamos nos aproximar um pouco mais do concreto para uma explicação. Digamos que eu tenho os seguintes arquivos:
class MyClass<T>
class MyClass<T>
MyClass<int>
Compilação separada significa que eu devo ser capaz de compilar foo.cpp independentemente do bar.cpp . O compilador realiza todo o trabalho árduo de análise, otimização e geração de código em cada unidade de compilação 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.
O bar.cpp nem precisa existir quando compilar o foo.cpp , mas ainda assim posso vincular o foo.o eu já tinha o bar.o que acabei de produzir, sem precisar recompilar o foo .cpp . O foo.cpp pode até ser compilado em uma biblioteca dinâmica, distribuída em outro lugar sem o foo.cpp e vinculada ao código que eles escrevem anos depois que eu escrevi o foo.cpp .
"Polimorfismo no estilo de instanciação" significa que o modelo
MyClass<T>
não é realmente uma classe genérica que pode ser compilada em código que possa funcionar com qualquer valor deT
. Que gostaria de acrescentar sobrecarga como o boxe, precisando passar ponteiros de função para alocadores e construtores, etc. a intenção de modelos C ++ é evitar ter que escrever quase idênticoclass MyClass_int
,class MyClass_float
, etc, mas ainda ser capaz de acabar com código compilado que é principalmente como se tivesse escrito cada versão separadamente. Portanto, um modelo é literalmente um modelo; um modelo de classe não é uma classe, é uma receita para criar uma nova classe para cada umaT
que encontrarmos. Um modelo não pode ser compilado em código, apenas o resultado da instanciação do modelo pode ser compilado.Portanto, quando o foo.cpp é compilado, o compilador não pode ver o bar.cpp para saber o que
MyClass<int>
é necessário. Ele pode ver o modeloMyClass<T>
, mas não pode emitir código para ele (é um modelo, não uma classe). E quando bar.cpp é compilado, o compilador pode ver que precisa criar umMyClass<int>
, mas não pode ver o modeloMyClass<T>
(apenas sua interface em foo.h ), portanto não pode criá-lo.Se o próprio foo.cpp usar
MyClass<int>
, o código será gerado durante a compilação do foo.cpp ; portanto, quando o bar.o estiver vinculado ao foo.o, eles poderão ser conectados 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 modelo e instancia-lo nos tipos que desejar; ele pode usar apenas versões pré-existentes da classe de 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 sendo filtradas durante a vinculação. Além da enorme sobrecarga e das dificuldades extremas que essa abordagem enfrentaria, porque os recursos de "modificador de tipo", como ponteiros e matrizes, permitem que apenas os tipos internos gerem um número infinito de tipos, o que acontece quando agora estendo meu programa adicionando:
class BazPrivate
e usaMyClass<BazPrivate>
Não há como isso funcionar, a menos que
MyClass<T>
MyClass<T>
, para que o compilador possa gerarMyClass<BazPrivate>
durante a compilação do baz.cpp .Ninguém gosta de (1), porque os sistemas de compilação de análise de programa inteiro levam uma eternidade para compilar e porque torna impossível distribuir bibliotecas compiladas sem o código-fonte. Então, temos (2) em vez disso.
fonte
Os modelos precisam ser instanciados pelo compilador antes de realmente compilá-los no código do objeto. Essa instanciação só pode ser alcançada se os argumentos do modelo forem conhecidos. Agora imagine um cenário em que uma função de modelo seja declarada
a.h
, definidaa.cpp
e usada emb.cpp
. Quandoa.cpp
é compilado, não é necessariamente conhecido que a próxima compilaçãob.cpp
exigirá uma instância do modelo, muito menos qual instância específica seria essa. Para mais arquivos de cabeçalho e de origem, a situação pode se complicar rapidamente.Pode-se argumentar que os compiladores podem ser mais inteligentes para "olhar adiante" 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 isso com antecipação. Como Anton apontou, alguns compiladores suportam declarações de exportação explícitas de instanciações de modelo, mas nem todos os compiladores o suportam (ainda?).
fonte
Na verdade, antes do C ++ 11, o padrão definia a
export
palavra - chave que tornaria possível declarar modelos em um arquivo de cabeçalho e implementá-los em outro local.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 exigiram que você escrevesse modelos em arquivos de cabeçalho, porque o compilador precisa da definição de modelo para instanciação adequada (como outros já apontaram).
Como resultado, o comitê padrão da ISO C ++ decidiu remover o
export
recurso de modelos com o C ++ 11.fonte
export
realmente teria dado nós, eo que não ... e agora estou inteiramente de acordo com as pessoas EDG: Não teria nos trouxe que a maioria das pessoas (eu em '11 incluído) acho que sim, e o padrão C ++ é melhor sem ele.Embora o C ++ padrão não exija esse requisito, alguns compiladores exigem que todos os modelos de função e classe precisem ser disponibilizados em todas as unidades de tradução usadas. Com efeito, para esses compiladores, os corpos das funções de modelo devem ser disponibilizados em um arquivo de cabeçalho. Repetindo: isso significa que esses compiladores não permitirão que sejam definidos em arquivos que não sejam de cabeçalho, como arquivos .cpp
Há uma palavra-chave de exportação que deve atenuar esse problema, mas não chega nem perto de ser portátil.
fonte
Os modelos devem ser usados nos cabeçalhos porque o compilador precisa instanciar versões diferentes do código, dependendo dos parâmetros fornecidos / deduzidos para os parâmetros do modelo. Lembre-se de que um modelo não representa código diretamente, mas um modelo para várias versões desse código. Ao compilar uma função não modelo em um
.cpp
arquivo, você está compilando uma função / classe concreta. Esse não é o caso dos modelos, que podem ser instanciados com diferentes tipos, ou seja, o código concreto deve ser emitido ao substituir os parâmetros do modelo por tipos concretos.Havia um recurso com a
export
palavra - chave que deveria ser usado para compilação separada. Oexport
recurso foi descontinuadoC++11
e, no AFAIK, apenas um compilador o implementou. Você não deve fazer usoexport
. A compilação separada não é possível na,C++
ouC++11
talvez naC++17
, se os conceitos entrarem, poderíamos ter uma maneira de compilação separada.Para que seja possível obter uma compilação separada, é necessário verificar o corpo do modelo separadamente. Parece que uma solução é possível com conceitos. Dê uma olhada neste artigo recentemente apresentado na reunião do comitê de padrões. Eu 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 compilação separado para modelos, acho que também é um problema que surge com a migração para os módulos, que estão sendo trabalhados no momento.
fonte
Isso significa que a maneira mais portátil de definir implementações de método de classes de modelo é defini-las dentro da definição de classe de modelo.
fonte
Embora haja muitas boas explicações acima, estou perdendo uma maneira prática de separar modelos em cabeçalho e corpo.
Minha principal preocupação é evitar a recompilação de todos os usuários de modelo, quando altero sua definição.
Ter todas as instanciações de modelo no corpo do modelo não é uma solução viável para mim, pois o autor do modelo pode não saber tudo se seu uso e o usuário do modelo podem não ter o direito de modificá-lo.
Adotei 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 cabeçalho (gerado a partir do modelo UML). Seu corpo contém a instanciação (que termina em uma biblioteca vinculada ao final).
Cada usuário do modelo inclui esse arquivo de cabeçalho e usa o typedef.
Um exemplo esquemático:
MyTemplate.h:
MyTemplate.cpp:
MyInstantiatedTemplate.h:
MyInstantiatedTemplate.cpp:
main.cpp:
Dessa forma, apenas as instanciações do modelo precisarão ser recompiladas, nem todos os usuários (e dependências) do modelo.
fonte
MyInstantiatedTemplate.h
arquivo e doMyInstantiatedTemplate
tipo adicionado . É um pouco mais limpo se você não usar isso, imho. Verificação geral minha resposta sobre uma questão diferente mostrando isso: stackoverflow.com/a/41292751/4612476Apenas para adicionar algo digno de nota aqui. Pode-se definir métodos de uma classe de modelo muito bem no arquivo de implementação quando eles não são modelos de função.
myQueue.hpp:
myQueue.cpp:
fonte
isEmpty
de nenhuma outra unidade de tradução além demyQueue.cpp
...Se a preocupação for o tempo extra de compilação e o tamanho do binário produzido pela compilação do .h como parte de todos os módulos .cpp que o utilizam, em muitos casos, o que você pode fazer é fazer com que a classe de modelo descenda de uma classe base não templatizada para partes não dependentes de tipo da interface e essa classe base pode ter sua implementação no arquivo .cpp.
fonte
class XBase
onde eu preciso implementar umtemplate class X
, colocando as partes dependentes do tipoX
e todo o restoXBase
.Isso é exatamente correto porque o compilador precisa saber que tipo é para alocação. Portanto, classes de modelo, funções, enumerações, etc. também devem ser implementadas no arquivo de cabeçalho para que sejam tornadas públicas ou parte de uma biblioteca (estática ou dinâmica) porque os arquivos de cabeçalho NÃO são compilados, diferentemente dos arquivos c / cpp que estamos. Se o compilador não souber o tipo, não será possível compilá-lo. No .Net, é possível porque todos os objetos derivam da classe Object. Este não é .Net.
fonte
O compilador gerará código para cada instanciação de modelo quando você usar um modelo durante a etapa de compilação. No processo de compilação e vinculação, os arquivos .cpp são convertidos em objeto puro ou código de máquina, que contém referências ou símbolos indefinidos, porque os arquivos .h incluídos no seu main.cpp ainda não têm implementação. Eles estão prontos para serem vinculados a outro arquivo de objeto que define uma implementação para o seu modelo e, portanto, você tem um executável a.out completo.
No entanto, como os modelos precisam ser processados na etapa de compilação para gerar código para cada instanciação de modelo que você definir, simplesmente compilar um modelo separado do arquivo de cabeçalho não funcionará porque eles sempre andam de mãos dadas, pelo mesmo motivo que cada instanciação de modelo é uma classe totalmente nova literalmente. Em uma classe regular, é possível separar. Hepp. Porque. H é um blueprint dessa classe e o. Cpp é a implementação bruta, para que todos os arquivos de implementação possam ser compilados e vinculados regularmente, no entanto, usando os modelos. H é um blueprint de como a classe não deve ter a aparência do objeto, o que significa que um arquivo .cpp de modelo não é uma implementação regular bruta de uma classe; é simplesmente um modelo para uma classe; portanto, qualquer implementação de um arquivo de modelo .h pode '
Portanto, os modelos nunca são compilados separadamente e são compilados apenas onde você tiver uma instanciação concreta em outro arquivo de origem. No entanto, a instanciação concreta precisa conhecer a implementação do arquivo de modelo, porque simplesmente modificar o
typename T
usar um tipo concreto no arquivo .h não fará o trabalho porque o que .cpp está lá para vincular, não consigo encontrá-lo mais tarde porque lembre-se de que os modelos são abstratos e não podem ser compilados, então sou forçado para fornecer a implementação agora, para que eu saiba o que compilar e vincular, e agora que tenho a implementação, ela será vinculada ao arquivo de origem. Basicamente, no momento em que instanciamos um modelo, preciso criar uma classe totalmente nova, e não posso fazer isso se não souber como deve ser essa classe ao usar o tipo que forneço, a menos que notifique o compilador de a implementação do modelo, agora o compilador pode substituirT
pelo meu tipo e criar uma classe concreta que está pronta para ser compilada e vinculada.Resumindo, modelos são modelos para a aparência das classes, classes são modelos para a aparência de um objeto. Não consigo compilar modelos separados de sua instanciação concreta porque o compilador compila apenas tipos concretos, ou seja, modelos pelo menos em C ++, é pura abstração de linguagem. Temos que des-abstrair modelos, por assim dizer, e fazemos isso dando a eles um tipo concreto para lidar, para que nossa abstração de modelo possa se transformar em um arquivo de classe regular e, por sua vez, possa ser compilado normalmente. A separação do arquivo .h do modelo e do arquivo .cpp do modelo não faz sentido. Não faz sentido, porque a separação de .cpp e .h é apenas onde os .cpp podem ser compilados individualmente e vinculados individualmente, com modelos, pois não podemos compilá-los separadamente, porque os modelos são uma abstração,
O significado de
typename T
get é substituído durante a etapa de compilação, e não na etapa de vinculação, por isso, se eu tentar compilar um modelo semT
ser substituído como um tipo de valor concreto que é completamente sem sentido para o compilador e, como resultado, o código do objeto não pode ser criado porque não sabe o queT
é.É tecnicamente possível criar algum tipo de funcionalidade que salvará o arquivo template.cpp e alterará os tipos quando os encontrar em outras fontes. Acho que o padrão possui uma palavra-chave
export
que permite colocar modelos em um arquivo separado. arquivo cpp, mas não muitos compiladores realmente implementam isso.Apenas uma observação, ao fazer especializações para uma classe de modelo, você pode separar o cabeçalho da implementação, porque uma especialização por definição significa que eu sou especializado em um tipo concreto que pode ser compilado e vinculado individualmente.
fonte
Uma maneira de ter uma implementação separada é a seguinte.
inner_foo tem as declarações de encaminhamento. foo.tpp possui a implementação e inclui inner_foo.h; e foo.h terá apenas uma linha, para incluir foo.tpp.
No tempo de compilação, 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. Dessa forma, não há limitações e a nomeação é 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 avançadas da classe em * .tpp. Isso é irritante ao escrever código em qualquer IDE ou ao usar o YouCompleteMe ou outros.
fonte
Eu sugiro olhar para esta página do gcc que discute as vantagens e desvantagens entre o modelo "cfront" e "borland" para instanciações de modelos.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
O modelo "borland" corresponde ao que o autor sugere, fornecendo a definição completa do modelo e compilando as coisas várias vezes.
Ele contém recomendações explícitas sobre o uso da instanciação manual e automática de modelos. Por exemplo, a opção "-repo" pode ser usada para coletar modelos que precisam ser instanciados. Ou outra opção é desativar instâncias automáticas de modelos usando "-fno-implicit-templates" para forçar a instanciação manual de modelos.
Na minha experiência, eu confio nos modelos C ++ Standard Library e Boost sendo instanciados para cada unidade de compilação (usando uma biblioteca de modelos). Para minhas grandes classes de modelos, faço instanciação manual de modelos, uma vez, para os tipos de que preciso.
Essa é minha abordagem porque estou fornecendo um programa de trabalho, não uma biblioteca de modelos para uso em outros programas. O autor do livro, Josuttis, trabalha muito em bibliotecas de modelos.
Se eu estivesse realmente preocupado com a velocidade, acho que exploraria o uso dos cabeçalhos pré-compilados https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
que está ganhando suporte em muitos compiladores. No entanto, acho que os cabeçalhos pré-compilados seriam difíceis com os arquivos de cabeçalho do modelo.
fonte
Outro motivo pelo qual é uma boa ideia escrever declarações e definições nos arquivos de cabeçalho é a legibilidade. Suponha que exista uma função de modelo no Utility.h:
E no Utility.cpp:
Isso requer que todas as classes T aqui implementem o operador less than (<). Irá gerar um erro de compilador quando você comparar duas instâncias de classe que não implementaram o "<".
Portanto, se você separar a declaração e a definição do modelo, não poderá ler apenas o arquivo de cabeçalho para ver os meandros desse modelo para usar esta API em suas próprias classes, embora o compilador lhe diga isso caso em que operador precisa ser substituído.
fonte
Você pode realmente definir sua classe de modelo dentro de um arquivo .template em vez de um arquivo .cpp. Quem está dizendo que você só pode defini-lo dentro de um arquivo de cabeçalho está errado. Isso é algo que funciona desde o c ++ 98.
Não se esqueça de fazer com que seu compilador trate seu arquivo .template como um arquivo c ++ para manter o senso intelectual.
Aqui está um exemplo disso para uma classe de matriz dinâmica.
Agora, dentro do arquivo .template, você define suas funções exatamente como faria normalmente.
fonte