Por que os modelos podem ser implementados apenas no arquivo de cabeçalho?


1778

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


13
Embora seja verdade que colocar todas as definições de função de modelo no arquivo de cabeçalho seja provavelmente a maneira mais conveniente de usá-las, ainda não está claro o que "inline" está fazendo nessa citação. Não é necessário usar funções embutidas para isso. "Inline" não tem absolutamente nada a ver com isso.
AnT

7
O livro está desatualizado.
gerardw

1
Um modelo não é como uma função que pode ser compilada em código de bytes. É apenas um padrão para gerar essa função. Se você colocar um modelo por conta própria em um arquivo * .cpp, não haverá nada para compilar. Além disso, a instanciação explícita não é realmente um modelo, mas o ponto de partida para criar uma função do modelo que termina no arquivo * .obj.
Dgrat 28/05/19

5
Eu sou o único que se sente que o conceito de modelo é aleijado em C ++ devido a isso ...?
DragonGamer

Respostas:


1559

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:

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

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

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

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

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

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

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

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

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


96
Na verdade, a instanciação explícita precisa estar em um arquivo .cpp que tenha acesso às definições para todas as funções de membro do Foo, e não no cabeçalho.
Mankarse 28/05

11
"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". Mas por que uma implementação em o arquivo .cpp não está acessível para o compilador? Um compilador também pode acessar informações .cpp, de que outra forma as transformaria em arquivos .obj? EDIT: a resposta a esta pergunta está no link fornecido nesta resposta ...
xcrypt

31
Eu não acho que isso explica a pergunta que claramente, a principal coisa está obviamente relacionado com a unidade de compilação que não é mencionado neste post
zinking

6
@ Gabson: estruturas e classes são equivalentes, com a exceção de que o modificador de acesso padrão para classes é "privado", enquanto é público para estruturas. Existem outras pequenas diferenças que você pode aprender olhando para esta pergunta .
Luc Touraille

3
Eu adicionei uma frase no início desta resposta para esclarecer que a pergunta é baseada em uma premissa falsa. Se alguém perguntar "Por que o X é verdadeiro?" quando de fato X não é verdadeiro, devemos rejeitar rapidamente essa suposição.
Aaron McDaid

250

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.

template class vector<int>;

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.


51
Ugh. Boa resposta, mas nenhuma solução realmente limpa. Listar todos os tipos possíveis para um modelo parece não corresponder ao que um modelo deve ser.
Jiminion 17/07/2014

6
Isso pode ser bom em muitos casos, mas geralmente quebra o objetivo do modelo, cujo objetivo é permitir que você use a classe com qualquer uma typesem listá-las manualmente.
Tomáš Zato - Restabelece Monica

7
vectornã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.
UncleZeiv

Utilizado após a definição do modelo ", e todas as funções de membro foram definidas". Obrigado !
Vitt Volt

1
Sinto que estou perdendo alguma coisa ... Coloquei a instancia explícita para dois tipos no .cpparquivo da classe e as duas instanciações são mencionadas em outros .cpparquivos, e ainda recebo o erro de vinculação de que os membros não foram encontrados.
oarfish 4/04/19

250

É 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:

  • foo.h
    • declara a interface de class MyClass<T>
  • foo.cpp
    • define a implementação de class MyClass<T>
  • bar.cpp
    • usa 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 de T. 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êntico class 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 uma Tque 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 modelo MyClass<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 um MyClass<int>, mas não pode ver o modelo MyClass<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:

  • baz.cpp
    • declara e implementa class BazPrivatee usaMyClass<BazPrivate>

Não há como isso funcionar, a menos que

  1. É necessário recompilar o foo.cpp toda vez que alteramos qualquer outro arquivo no programa , caso ele tenha adicionado uma nova instanciação nova deMyClass<T>
  2. Exija que o baz.cpp contenha (possivelmente através do cabeçalho incluído) o modelo completo de MyClass<T>, para que o compilador possa gerar MyClass<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.


50
citação enfatizada: um modelo é literalmente um modelo; um modelo de classe não é uma classe, é uma receita para a criação de uma nova classe para cada T encontramos
v.oddou

Gostaria de saber, é possível executar instanciações explícitas de algum lugar que não seja o cabeçalho ou o arquivo de origem da classe? Por exemplo, faça-os em main.cpp?
precisa saber é o seguinte

1
@Birger Você poderá fazê-lo a partir de qualquer arquivo que tenha acesso à implementação completa do modelo (seja porque está no mesmo arquivo ou através do cabeçalho incluído).
Ben

11
@ajeh Não é retórica. A pergunta é "por que você precisa implementar modelos em um cabeçalho?", Então expliquei as opções técnicas que a linguagem C ++ faz que levam a esse requisito. Antes de escrever minha resposta, outras pessoas já forneciam soluções alternativas que não são soluções completas, porque não pode haver uma solução completa. Achei que essas respostas seriam complementadas por uma discussão mais completa do ângulo "por que" da pergunta.
Ben

1
imagine dessa maneira pessoal ... se você não estivesse usando modelos (para codificar com eficiência o que precisava), estaria oferecendo apenas algumas versões dessa classe. então você tem 3 opções. 1) não use modelos. (como todas as outras classes / funções, ninguém se importa que outros não possam alterar os tipos) 2). usar modelos e documentar quais tipos eles podem usar. 3) dê a eles todo o bônus de implementação (fonte) 4). forneça a eles toda a fonte, caso desejem criar um modelo de outra classe;)
Puddle

81

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, definida a.cppe usada em b.cpp. Quando a.cppé compilado, não é necessariamente conhecido que a próxima compilação b.cppexigirá 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?).


1
"export" é padrão, mas é difícil de implementar, portanto a maioria das equipes do compilador ainda não o fez.
vava

5
a exportação não elimina a necessidade de divulgação da fonte, nem reduz as dependências de compilação, enquanto exige um grande esforço dos construtores de compiladores. Então, o próprio Herb Sutter pediu aos construtores de compiladores que 'esquecessem' a exportação. Como o investimento de tempo necessário seria melhor gastar em outros lugares ...
Pieter

2
Portanto, não acho que a exportação ainda não esteja implementada. Ele provavelmente nunca ter feito por ninguém mais do que EDG após os outros viram o tempo que levou, e quão pouco foi adquirido
Pieter

3
Se isso lhe interessa, o artigo chama-se "Por que não podemos permitir a exportação", está listado em seu blog ( gotw.ca/publications ), mas não há pdf lá (um google rápido deve aparecer)
Pieter

1
Ok, obrigado pelo bom exemplo e explicação. Aqui está minha pergunta: por que o compilador não pode descobrir onde o modelo é chamado e compilar esses arquivos antes de compilar o arquivo de definição? Eu posso imaginar que isso pode ser feito em um caso simples ... A resposta é que as interdependências atrapalham o pedido muito rápido?
Vlad

62

Na verdade, antes do C ++ 11, o padrão definia a exportpalavra - 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 exportrecurso de modelos com o C ++ 11.


6
... e um par de anos mais tarde, eu finalmente entendi o que exportrealmente 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.
DevSolar

4
@ DevSolar: este artigo é político, repetitivo e mal escrito. isso não é uma prosa comum de nível padrão lá. Desnecessariamente longo e chato, dizendo basicamente três vezes o mesmo em dezenas de páginas. Mas agora sou informado que a exportação não é exportação. Essa é uma boa informação!
precisa saber é o seguinte

1
@ v.oddou: Bom desenvolvedor e bom escritor técnico são duas habilidades separadas. Alguns podem fazer as duas coisas, muitos não. ;-)
DevSolar 25/04

@ v.oddou O artigo não está apenas mal escrito, é desinformação. Também é uma reviravolta na realidade: o que é realmente um argumento extremamente forte para as exportações é misturado de forma a parecer que é contra a exportação: “descobrindo vários buracos relacionados ao ODR no padrão na presença de exportação. Antes da exportação, as violações do ODR não precisavam ser diagnosticadas pelo compilador. Agora é necessário porque você precisa combinar estruturas de dados internas de diferentes unidades de tradução e não pode combiná-las se elas realmente estiverem representando coisas diferentes; portanto, é necessário fazer a verificação. ”
curiousguy

" deve agora adicionar em qual unidade de tradução estava quando aconteceu " Duh. Quando você é forçado a usar argumentos que não dão certo, você não tem nenhum argumento. Claro que você mencionará nomes de arquivos nos seus erros, qual é o problema? Que alguém se apaixona por esse BS é incompreensível. " Até especialistas como James Kanze acham difícil aceitar que a exportação seja realmente assim. "
curiousguy

34

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.


Por que não consigo implementá-los no arquivo .cpp com a palavra-chave "inline"?
MainID 30/01/09

2
Você pode e nem precisa colocar "inline". Mas você seria capaz de usá-los apenas nesse arquivo cpp e em nenhum outro lugar.
vava

10
Essa é quase a resposta mais precisa , exceto que "isso significa que esses compiladores não permitirão que sejam definidos em arquivos que não sejam de cabeçalho, como arquivos .cpp", são evidentemente falsos.
Lightness Races em órbita

28

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 .cpparquivo, 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 exportpalavra - chave que deveria ser usado para compilação separada. O exportrecurso foi descontinuado C++11e, no AFAIK, apenas um compilador o implementou. Você não deve fazer uso export. A compilação separada não é possível na, C++ou C++11talvez na C++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.


15

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.

template < typename ... >
class MyClass
{

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

15

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:

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

Dessa forma, apenas as instanciações do modelo precisarão ser recompiladas, nem todos os usuários (e dependências) do modelo.


1
Eu gosto dessa abordagem, com exceção do MyInstantiatedTemplate.harquivo e do MyInstantiatedTemplatetipo 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/4612476
Cameron Tacklind

Isso leva o melhor de dois mundos. Desejo que esta resposta tenha uma classificação mais alta! Veja também o link acima para uma implementação ligeiramente mais limpa da mesma idéia.
Wormer

8

Apenas 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:

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
Se realmente é verdade, sua resposta deve ser verificada como correta.Por que alguém precisa de todas essas coisas hacky de voodo, se você pode apenas definir métodos que não são membros do modelo em .cpp?
Michael IV

Bem, isso não funciona.Pelo menos no MSVC 2019, obtendo um símbolo externo não resolvido para uma função de membro da classe de modelo.
Michael IV

Não tenho o MSVC 2019 para testar. Isso é permitido pelo padrão C ++. Agora, o MSVC é notório por nem sempre aderir às regras. Se ainda não o fez, tente Configurações do projeto -> C / C ++ -> Idioma -> Modo de conformidade -> Sim (permissivo).
Nikos

1
Este exemplo exato funciona, mas você não pode ligar isEmptyde nenhuma outra unidade de tradução além de myQueue.cpp...
MM

7

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.


2
Essa resposta deve ser modificada muito mais. Eu " independentemente " descobri sua mesma abordagem e estava procurando especificamente alguém que já a tivesse usado, pois estou curioso para saber se é um padrão oficial e se tem um nome. Minha abordagem é implementar um class XBaseonde eu preciso implementar um template class X, colocando as partes dependentes do tipo Xe todo o resto XBase.
Fabio A.

6

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.


5
"arquivos de cabeçalho NÃO são compilados" - é uma maneira realmente estranha de descrevê-lo. Os arquivos de cabeçalho podem fazer parte de uma unidade de tradução, assim como um arquivo "c / cpp".
Flexo

2
De fato, é quase o oposto da verdade, que é o fato de que os arquivos de cabeçalho são frequentemente compilados muitas vezes, enquanto um arquivo de origem geralmente é compilado uma vez.
Xxxxon

6

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 otypename Tusar 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 substituir Tpelo 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 Tget é substituído durante a etapa de compilação, e não na etapa de vinculação, por isso, se eu tentar compilar um modelo sem Tser 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 que Té.

É 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 exportque 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.


4

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

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


2
s / inner_foo / foo / ge inclua foo.tpp no ​​final de foo.h. Um arquivo a menos.

1

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.


-2

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:

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

E no Utility.cpp:

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

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.


-7

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.

#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

Agora, dentro do arquivo .template, você define suas funções exatamente como faria 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
A maioria das pessoas definiria um arquivo de cabeçalho como qualquer coisa que propague definições para os arquivos de origem. Então você pode ter decidido usar a extensão de arquivo ".template", mas criou um arquivo de cabeçalho.
Tommy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.