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

Jan 30 2009

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

1635 LucTouraille Jan 30 2009 at 17:26

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 */}
}

Consequentemente, o compilador precisa ter acesso à implementação dos métodos, para instanciá-los com o argumento do template (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 e, em seguida, 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 está 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 de que você precisa:

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 no C ++ Super-FAQ sobre este assunto .

271 Ben May 11 2013 at 10:54

É devido ao 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 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 devo ser capaz de compilar foo.cpp independentemente de bar.cpp . O compilador faz 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 a análise de todo o programa. É apenas o vinculador que precisa lidar com todo o programa de uma vez, e o trabalho do vinculador é substancialmente mais fácil.

bar.cpp nem precisa existir quando eu compilar foo.cpp , mas eu ainda devo ser capaz de vincular o foo.o que eu já tinha junto com o bar.o Acabei de produzir, sem precisar recompilar foo .cpp . foo.cpp poderia até ser compilado em uma biblioteca dinâmica, distribuída em outro lugar sem foo.cpp e vinculado ao código que eles escreveram anos depois que escrevi foo.cpp .

"Polimorfismo de estilo de instanciação" significa que o modelo MyClass<T>não é realmente uma classe genérica que pode ser compilada para um código que pode funcionar para 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 Tque encontramos. Um modelo não pode ser compilado em código, apenas o resultado da instanciação do modelo pode ser compilado.

Portanto, quando foo.cpp é compilado, o compilador não pode ver 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 isso (é 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 usa MyClass<int>, então o código para isso será gerado durante a compilação do foo.cpp , então quando bar.o está vinculado a foo.o eles podem 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 instanciá-lo nos tipos que desejar; ele só pode usar versões pré-existentes da classe modelada que o autor de foo.cpp pensou em fornecer.

Você pode pensar que, ao compilar um modelo, o compilador deve "gerar todas as versões", com aquelas que nunca são usadas sendo filtradas durante a vinculação. Além da enorme sobrecarga e das dificuldades extremas que tal abordagem enfrentaria, porque recursos de "modificadores de tipo" como ponteiros e matrizes permitem que até mesmo os tipos integrados dêem origem a um número infinito de tipos, o que acontece quando agora eu estendo meu programa adicionando:

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

Não é possível que isso funcione, a menos que nós

  1. Tem que recompilar foo.cpp cada vez que alteramos qualquer outro arquivo no programa , no caso de adicionar uma nova instanciação deMyClass<T>
  2. Exija que baz.cpp contenha (possivelmente por meio de inclusão de cabeçalho) o modelo completo de MyClass<T>, para que o compilador possa gerar MyClass<BazPrivate>durante a compilação de baz.cpp .

Ninguém gosta de (1), porque os sistemas de compilação de análise de programa completo demoram uma eternidade para compilar e porque torna impossível distribuir bibliotecas compiladas sem o código-fonte. Portanto, em vez disso, temos (2).

252 MaHuJa Aug 13 2009 at 20:49

Muitas respostas corretas aqui, mas eu queria adicionar isto (para completar):

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

Editar: Adicionando exemplo de instanciação de modelo explícito. Usado depois que o modelo foi definido e todas as funções de membro foram definidas.

template class vector<int>;

Isso irá instanciar (e, portanto, disponibilizar para o vinculador) a classe e todas as suas funções de membro (apenas). Sintaxe semelhante funciona para funções de modelo, portanto, se você tiver sobrecargas de operadores de não membros, pode ser necessário fazer o mesmo para elas.

O exemplo acima é bastante inútil, uma vez que o vetor é totalmente definido nos cabeçalhos, exceto quando um arquivo de inclusão comum (cabeçalho pré-compilado?) Usa extern template class vector<int>para evitar que seja instanciado em todos os outros (1000?) Arquivos que usam o vetor.

86 DavidHanak Jan 30 2009 at 17:23

Os modelos precisam ser instanciados pelo compilador antes de realmente compilá-los no código do objeto. Esta instanciação só pode ser alcançada se os argumentos do modelo forem conhecidos. Agora imagine um cenário onde uma função de modelo é declarada em a.h, definida em 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. Para mais arquivos de cabeçalho e de origem, a situação pode ficar mais complicada rapidamente.

Pode-se argumentar que os compiladores podem ser mais inteligentes para "prever" todos os usos do modelo, mas tenho certeza de que não seria difícil criar cenários recursivos ou complicados de outra forma. AFAIK, os compiladores não olham para o futuro. Como Anton apontou, alguns compiladores suportam declarações de exportação explícitas de instanciações de template, mas nem todos os compiladores suportam (ainda?).

65 DevSolar Jan 30 2009 at 20:38

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

Nenhum dos compiladores populares implementou esta palavra-chave. O único que 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 do modelo para a instanciação adequada (como outros já apontaram).

Como resultado, o comitê de padrões ISO C ++ decidiu remover o exportrecurso de modelos com C ++ 11.

34 AntonGogolev Jan 30 2009 at 17:15

Embora o C ++ padrão não tenha esse requisito, alguns compiladores exigem que todos os modelos de função e classe sejam disponibilizados em cada unidade de tradução em que são usados. Na verdade, 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

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

29 GermánDiago May 12 2013 at 23:42

Os modelos devem ser usados ​​nos cabeçalhos porque o compilador precisa instanciar diferentes versões do código, dependendo dos parâmetros fornecidos / 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 função não-template em um .cpparquivo, está compilando uma função / classe 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 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 está obsoleto C++11e, AFAIK, apenas um compilador o implementou. Você não deve fazer uso de export. A compilação separada não é possível em C++ou, C++11mas talvez em C++17, se os conceitos forem incluídos, poderíamos ter alguma forma de compilação separada.

Para que uma compilação separada seja alcançada, 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, já que você ainda precisa instanciar o código para o código do modelo no código do usuário.

O problema de compilação separada para modelos Eu acho que também é um problema que está surgindo com a migração para módulos, que está sendo trabalhada no momento.

EDITAR: A partir de agosto de 2020 Módulos já são uma realidade para C ++: https://en.cppreference.com/w/cpp/language/modules

18 lafrecciablu May 12 2016 at 21:02

Embora haja muitas boas explicações acima, estou perdendo uma maneira prática de separar os modelos em cabeçalho e corpo.
Minha principal preocupação é evitar a recompilação de todos os usuários do template, quando eu mudar sua definição.
Ter todas as instanciações do 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 pode não ter o direito de modificá-lo.
Usei 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 no 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;
}

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

16 Benoît Jan 30 2009 at 17:53

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

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
9 Nikos Jul 19 2018 at 07:49

Só para adicionar algo digno de nota aqui. Pode-se definir métodos de uma classe modelada 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;

    ...
}
7 EricShaw Jul 27 2016 at 12:01

Se a preocupação for o tempo extra de compilação e o inchaço do tamanho binário produzido pela compilação 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 classe de modelo descenda de uma classe base não padronizada partes não dependentes de tipo da interface, e essa classe base pode ter sua implementação no arquivo .cpp.

6 Robert Sep 17 2011 at 10:40

Isso é exatamente correto porque o compilador precisa saber que tipo é para alocação. Portanto, classes de modelo, funções, enums, etc. devem ser implementados também no arquivo de cabeçalho se for para ser tornado público ou parte de uma biblioteca (estática ou dinâmica) porque os arquivos de cabeçalho 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 objetos derivam da classe Object. Este não é .Net.

6 MosheRabaev Jul 19 2016 at 08:10

O compilador irá 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 em 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 seu modelo e, portanto, você tem um executável a.out completo.

No entanto, uma vez que os modelos precisam ser processados ​​na etapa de compilação a fim de gerar código para cada instanciação de modelo que você definir, simplesmente compilar um modelo separado de seu arquivo de cabeçalho não funcionará porque eles sempre andam lado a lado, pela própria razão que cada instanciação de modelo é uma classe totalmente nova literalmente. Em uma classe regular, você pode separar .h e .cpp porque .h é um projeto dessa classe e .cpp é a implementação bruta, de modo que todos os arquivos de implementação podem ser compilados e vinculados regularmente, no entanto, o uso de modelos .h é um projeto de como a classe não deve ter a aparência do objeto, o que significa que um arquivo de modelo .cpp não é uma implementação regular bruta de uma classe, é simplesmente um projeto para uma classe, portanto, qualquer implementação de um arquivo de modelo .h não pode ser compilada porque você precisa de algo concreto para compilar, os modelos são abstratos nesse sentido.

Portanto, os modelos nunca são compilados separadamente e só são compilados sempre que houver uma instanciação concreta em algum outro arquivo de origem. No entanto, a instanciação concreta precisa saber a implementação do arquivo de modelo, porque simplesmente modificar o typename Tuso de um tipo concreto no arquivo .h não fará o trabalho porque o .cpp está lá para vincular, eu não consigo encontrar mais tarde, porque lembre-se de que os modelos são abstratos e não podem ser compilados, então sou forçado a fornecer a implementação agora, então eu sei o que compilar e vincular, e agora que tenho a implementação, ela é vinculada ao arquivo-fonte anexo. Basicamente, no momento em que instanciar um modelo, preciso criar uma classe totalmente nova, e não posso fazer isso se não souber como essa classe deve ser ao usar o tipo que forneço, a menos que avise o compilador de a implementação do modelo, então agora o compilador pode substituir Tpelo meu tipo e criar uma classe concreta que está pronta para ser compilada e vinculada.

Para resumir, os modelos são projetos de como as classes devem ser, as classes são projetos de como um objeto deve ser. Não posso compilar templates separados de sua instanciação concreta porque o compilador só compila tipos concretos, em outras palavras, templates pelo menos em C ++, é pura abstração de linguagem. Temos que retirar os modelos de abstração, por assim dizer, e fazemos isso dando a eles um tipo concreto com o qual lidar, de modo que nossa abstração de modelo possa se transformar em um arquivo de classe regular e, por sua vez, pode ser compilado normalmente. Separar o arquivo de modelo .h e o arquivo de modelo .cpp não faz sentido. Não faz sentido porque a separação de .cpp e .h é apenas onde o .cpp pode ser compilado individualmente e vinculado individualmente, com modelos, uma vez que não podemos compilá-los separadamente, porque os modelos são uma abstração, portanto, somos sempre forçados a coloque a abstração sempre junto com a instanciação concreta onde a instanciação concreta sempre tem que saber sobre o tipo que está sendo usado.

Significa que typename Tget é substituído durante a etapa de compilação, não na etapa de vinculação, então 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 de 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 trocará os tipos quando os encontrar em outras fontes, eu acho que o padrão tem uma palavra-chave exportque permitirá que você coloque os modelos em um 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 estou me especializando para um tipo concreto que pode ser compilado e vinculado individualmente.

4 Pranay May 13 2017 at 08:42

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 tem a implementação e inclui inner_foo.h; e foo.h terá apenas uma linha, para incluir foo.tpp.

Em tempo de compilação, o conteúdo de foo.h é copiado para foo.tpp e então todo o arquivo é copiado para foo.h após o qual ele compila. Dessa 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 futuras de classe em * .tpp. Isso é irritante ao escrever código em qualquer IDE ou ao usar o YouCompleteMe ou outros.

1 Juan Feb 18 2020 at 03:30

Eu sugiro olhar para esta página do gcc que discute as compensações entre os modelos "cfront" e "borland" para instanciações de template.

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 tendo coisas compiladas várias vezes.

Ele contém recomendações explícitas sobre o uso de instanciação de modelo manual e automática. Por exemplo, a opção "-repo" pode ser usada para coletar modelos que precisam ser instanciados. Ou outra opção é desabilitar as instanciações automáticas de modelos usando "-fno-implicit-templates" para forçar a instanciação manual de modelos.

Em minha experiência, conto com a biblioteca padrão C ++ e os modelos Boost sendo instanciados para cada unidade de compilação (usando uma biblioteca de modelos). Para minhas grandes classes de modelo, faço a instanciação manual do modelo, uma vez, para os tipos de que preciso.

Esta é a minha abordagem porque estou fornecendo um programa funcional, não uma biblioteca de modelos para uso em outros programas. O autor do livro, Josuttis, trabalha muito com bibliotecas de modelos.

Se eu estivesse realmente preocupado com a velocidade, acho que exploraria o uso de cabeçalhos pré-compilados https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

que está ganhando suporte em muitos compiladores. No entanto, acho que cabeçalhos pré-compilados seriam difíceis com arquivos de cabeçalho de modelo.