Estou recebendo erros ao tentar compilar uma classe de modelo C ++ que é dividida entre um arquivo .hpp
e .cpp
:
$ g++ -c -o main.o main.cpp
$ g++ -c -o stack.o stack.cpp
$ g++ -o main main.o stack.o
main.o: In function `main':
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'
collect2: ld returned 1 exit status
make: *** [program] Error 1
Aqui está o meu código:
stack.hpp :
#ifndef _STACK_HPP
#define _STACK_HPP
template <typename Type>
class stack {
public:
stack();
~stack();
};
#endif
stack.cpp :
#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
main.cpp :
#include "stack.hpp"
int main() {
stack<int> s;
return 0;
}
ld
é claro que está correto: os símbolos não estão em stack.o
.
A resposta a essa pergunta não ajuda, pois já estou fazendo o que ele diz.
Isso pode ajudar, mas não quero mover todos os métodos para o .hpp
arquivo - não deveria, deveria?
A única solução razoável é mover tudo no .cpp
arquivo para o .hpp
arquivo e simplesmente incluir tudo, em vez de vincular como um arquivo de objeto independente? Isso parece muito feio! Nesse caso, posso muito bem voltar ao meu estado anterior e renomear stack.cpp
para stack.hpp
e terminar com ele.
Respostas:
Não é possível escrever a implementação de uma classe de modelo em um arquivo cpp separado e compilar. Todas as maneiras de fazer isso, se alguém alegar, são soluções alternativas para imitar o uso de arquivo cpp separado, mas praticamente se você pretende escrever uma biblioteca de classes de modelo e distribuí-la com arquivos de cabeçalho e lib para ocultar a implementação, isso simplesmente não é possível .
Para saber por quê, vejamos o processo de compilação. Os arquivos de cabeçalho nunca são compilados. Eles são apenas pré-processados. O código pré-processado é então combinado com o arquivo cpp que é realmente compilado. Agora, se o compilador precisa gerar o layout de memória apropriado para o objeto, ele precisa saber o tipo de dados da classe de modelo.
Na verdade, deve ser entendido que a classe de modelo não é uma classe, mas um modelo para uma classe cuja declaração e definição é gerada pelo compilador em tempo de compilação após obter as informações do tipo de dados do argumento. Enquanto o layout da memória não puder ser criado, as instruções para a definição do método não podem ser geradas. Lembre-se de que o primeiro argumento do método de classe é o operador 'this'. Todos os métodos de classe são convertidos em métodos individuais com mutação de nome e o primeiro parâmetro como o objeto no qual opera. O argumento 'this' é o que realmente diz sobre o tamanho do objeto que, no caso da classe de modelo, não está disponível para o compilador, a menos que o usuário instancie o objeto com um argumento de tipo válido. Nesse caso, se você colocar as definições de método em um arquivo cpp separado e tentar compilá-lo, o próprio arquivo-objeto não será gerado com as informações da classe. A compilação não falhará, ela gerará o arquivo objeto, mas não gerará nenhum código para a classe de modelo no arquivo objeto. Esta é a razão pela qual o vinculador não consegue encontrar os símbolos nos arquivos de objeto e a compilação falha.
Agora, qual é a alternativa para ocultar detalhes importantes de implementação? Como todos sabemos, o objetivo principal por trás da separação da interface da implementação é ocultar os detalhes da implementação em formato binário. É aqui que você deve separar as estruturas de dados e algoritmos. Suas classes de modelo devem representar apenas estruturas de dados, não os algoritmos. Isso permite que você esconda detalhes de implementação mais valiosos em bibliotecas de classes não padronizadas, as classes dentro das quais funcionariam nas classes de modelo ou apenas as usariam para armazenar dados. A classe de modelo conteria na verdade menos código para atribuir, obter e definir dados. O resto do trabalho seria feito pelas classes de algoritmo.
Espero que esta discussão seja útil.
fonte
Ele é possível, desde que você sabe o que instantiations você está indo para necessidade.
Adicione o seguinte código no final de stack.cpp e ele funcionará:
Todos os métodos de pilha não-template serão instanciados e a etapa de vinculação funcionará bem.
fonte
template class stack<int>;
.Você pode fazer desta forma
Isso foi discutido em Daniweb
Também no FAQ, mas usando a palavra-chave de exportação C ++.
fonte
include
cpp
geralmente é uma péssima ideia carregar um arquivo. mesmo se você tiver um motivo válido para isso, o arquivo - que na verdade é apenas um cabeçalho glorificado - deve receber umahpp
ou alguma extensão diferente (por exemplotpp
) para deixar bem claro o que está acontecendo, remover a confusão sobremakefile
s direcionando arquivos reaiscpp
, etc..cpp
arquivo é uma ideia terrível?cpp
(oucc
, ouc
, ou qualquer outra coisa) indica que o arquivo é uma parte da implementação, que a unidade de tradução resultante (saída do pré-processador) é compilável separadamente e que o conteúdo do arquivo é compilado apenas uma vez. não indica que o arquivo é uma parte reutilizável da interface, para ser incluído arbitrariamente em qualquer lugar.#include
O processamento de um arquivo realcpp
preencheria rapidamente sua tela com erros de definição múltipla, e com razão. neste caso, como não é uma razão para#include
isso,cpp
era apenas a escolha errada de extensão..cpp
extensão para tal uso. Mas usar outra palavra.tpp
é completamente correto, o que serviria ao mesmo propósito, mas usar uma extensão diferente para um entendimento mais fácil / rápido?cpp
/cc
/ etc deve ser evitada, mas é uma boa idéia para usar algo diferentehpp
- por exemplotpp
,tcc
, etc. - assim você pode reutilizar o resto do nome do arquivo e indicam que otpp
arquivo, embora ele age como um cabeçalho, mantém a implementação fora de linha das declarações de modelo no correspondentehpp
. Portanto, este post começa com uma boa premissa - separar declarações e definições em 2 arquivos diferentes, o que pode ser mais fácil de grok / grep ou às vezes é necessário devido às dependências circulares do IME - mas termina mal sugerindo que o segundo arquivo tem uma extensão erradaNão, não é possível. Não sem a
export
palavra - chave, que para todos os efeitos e propósitos não existe.O melhor que você pode fazer é colocar as implementações de sua função em um arquivo ".tcc" ou ".tpp" e # incluir o arquivo .tcc no final de seu arquivo .hpp. No entanto, isso é meramente cosmético; ainda é o mesmo que implementar tudo em arquivos de cabeçalho. Este é simplesmente o preço que você paga pelo uso de modelos.
fonte
Acredito que haja duas razões principais para tentar separar o código modelado em um cabeçalho e um cpp:
Um é por mera elegância. Todos nós gostamos de escrever código fácil de ler, gerenciar e reutilizável posteriormente.
Outro é a redução dos tempos de compilação.
Atualmente (como sempre) estou codificando um software de simulação em conjunto com o OpenCL e gostamos de manter o código para que ele possa ser executado usando os tipos float (cl_float) ou double (cl_double) conforme necessário, dependendo da capacidade do HW. No momento, isso é feito usando um #define REAL no início do código, mas isso não é muito elegante. Alterar a precisão desejada requer a recompilação do aplicativo. Uma vez que não existem tipos de tempo de execução real, temos que conviver com isso por enquanto. Felizmente, os kernels do OpenCL são compilados em tempo de execução e um simples sizeof (REAL) nos permite alterar o tempo de execução do código do kernel de acordo.
O problema muito maior é que, embora a aplicação seja modular, ao desenvolver classes auxiliares (como aquelas que pré-calculam as constantes de simulação) também devem ser modeladas. Todas essas classes aparecem pelo menos uma vez no topo da árvore de dependências de classe, pois a classe de modelo final Simulation terá uma instância de uma dessas classes de fábrica, o que significa que praticamente toda vez que faço uma pequena alteração na classe de fábrica, todo o o software precisa ser reconstruído. Isso é muito chato, mas não consigo encontrar uma solução melhor.
fonte
Somente se você
#include "stack.cpp
no final destack.hpp
. Eu só recomendo essa abordagem se a implementação for relativamente grande e se você renomear o arquivo .cpp para outra extensão, para diferenciá-lo do código normal.fonte
cpp
(cc
ou qualquer outra), porque isso é um contraste gritante com sua função real. Em vez disso, deve receber uma extensão diferente que indica que é (A) um cabeçalho e (B) um cabeçalho a ser incluído na parte inferior de outro cabeçalho. Eu usotpp
para isso, que com folga também pode ficar parat
emp
tarde imp
lementation (definições out-of-line). Divaguei mais sobre isso aqui: stackoverflow.com/questions/1724036/…Às vezes, é possível ter a maior parte da implementação oculta no arquivo cpp, se você puder extrair a funcionalidade comum de todos os parâmetros do modelo em uma classe não modelo (possivelmente inseguro de tipo). O cabeçalho conterá chamadas de redirecionamento para essa classe. Abordagem semelhante é usada, ao lutar com o problema de "template bloat".
fonte
Se você sabe com quais tipos sua pilha será usada, você pode instanciá-los detalhadamente no arquivo cpp e manter todo o código relevante lá.
Também é possível exportá-los entre DLLs (!), Mas é bastante complicado obter a sintaxe correta (combinações específicas do MS de __declspec (dllexport) e a palavra-chave export).
Usamos isso em uma lib math / geom que templado double / float, mas tinha bastante código. (Eu pesquisei no Google na época, mas não tenho esse código hoje.)
fonte
O problema é que um modelo não gera uma classe real, é apenas um modelo que informa ao compilador como gerar uma classe. Você precisa gerar uma classe concreta.
A maneira fácil e natural é colocar os métodos no arquivo de cabeçalho. Mas existe outra maneira.
Em seu arquivo .cpp, se você tiver uma referência a cada instanciação de modelo e método de que necessita, o compilador irá gerá-los para uso em todo o projeto.
novo stack.cpp:
fonte
Você precisa ter tudo no arquivo hpp. O problema é que as classes não são realmente criadas até que o compilador veja que elas são necessárias para algum outro arquivo cpp - então ele deve ter todo o código disponível para compilar a classe modelada naquele momento.
Uma coisa que tenho tendência a fazer é tentar dividir meus modelos em uma parte genérica não modelada (que pode ser dividida entre cpp / hpp) e a parte de modelo específico do tipo que herda a classe não modelada.
fonte
Como os modelos são compilados quando necessário, isso força uma restrição para projetos de vários arquivos: a implementação (definição) de uma classe ou função de modelo deve estar no mesmo arquivo de sua declaração. Isso significa que não podemos separar a interface em um arquivo de cabeçalho separado e que devemos incluir interface e implementação em qualquer arquivo que use os modelos.
fonte
Outra possibilidade é fazer algo como:
Não gosto dessa sugestão por uma questão de estilo, mas pode ser adequada para você.
fonte
cpp
para evitar confusão com os arquivos de origem reais . As sugestões comuns incluemtpp
etcc
.A palavra-chave 'export' é a maneira de separar a implementação do modelo da declaração do modelo. Isso foi introduzido no padrão C ++ sem uma implementação existente. No devido tempo, apenas alguns compiladores realmente o implementaram. Leia informações detalhadas no artigo Inform IT sobre exportação
fonte
1) Lembre-se de que o principal motivo para separar os arquivos .h e .cpp é ocultar a implementação da classe como um código Obj compilado separadamente que pode ser vinculado ao código do usuário que inclui um .h da classe.
2) As classes não-modelo têm todas as variáveis concreta e especificamente definidas nos arquivos .h e .cpp. Assim, o compilador terá as informações necessárias sobre todos os tipos de dados usados na classe antes de compilar / traduzir gerar o objeto / código de máquina As classes de modelo não têm informações sobre o tipo de dados específico antes que o usuário da classe instancie um objeto passando os dados necessários tipo:
3) Somente após essa instanciação, o compilador gera a versão específica da classe de modelo para corresponder ao (s) tipo (s) de dados passado.
4) Portanto, .cpp NÃO pode ser compilado separadamente sem conhecer o tipo de dados específico do usuário. Portanto, ele deve permanecer como código-fonte dentro de “.h” até que o usuário especifique o tipo de dados necessário, então, ele pode ser gerado para um tipo de dados específico e compilado
fonte
O lugar onde você pode querer fazer isso é ao criar uma combinação de biblioteca e cabeçalho e ocultar a implementação para o usuário. Portanto, a abordagem sugerida é usar a instanciação explícita, porque você sabe o que se espera que o seu software entregue e pode ocultar as implementações.
Algumas informações úteis estão aqui: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019
Para o seu mesmo exemplo: Stack.hpp
stack.cpp
main.cpp
Resultado:
No entanto, não gosto totalmente dessa abordagem, porque permite que o aplicativo atire no próprio pé, passando tipos de dados incorretos para a classe modelada. Por exemplo, na função principal, você pode passar outros tipos que podem ser convertidos implicitamente para int como s.Push (1.2); e isso é ruim na minha opinião.
fonte
Estou trabalhando com o Visual studio 2010, se você gostaria de dividir seus arquivos em .h e .cpp, inclua o cabeçalho cpp no final do arquivo .h
fonte