É possível dividir classes C ++ modeladas em arquivos .hpp / .cpp?

96

Estou recebendo erros ao tentar compilar uma classe de modelo C ++ que é dividida entre um arquivo .hppe .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 .hpparquivo - não deveria, deveria?

A única solução razoável é mover tudo no .cpparquivo para o .hpparquivo 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.cpppara stack.hppe terminar com ele.

exscape
fonte
Existem duas ótimas soluções alternativas para quando você deseja realmente manter seu código oculto (em um arquivo binário) ou mantê-lo limpo. É necessário reduzir a generalidade, embora na primeira situação. É explicado aqui: stackoverflow.com/questions/495021/…
Sheric

Respostas:

151

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.

Sharjith N.
fonte
2
"deve ser entendido que a classe de modelo não é uma classe" - não era o contrário? O modelo de classe é um modelo. "Classe de modelo" às vezes é usada no lugar de "instanciação de um modelo" e seria uma classe real.
Xupicor
Apenas para referência, não é correto dizer que não há soluções alternativas! Separar estruturas de dados de métodos também é uma má ideia, pois é oposto ao encapsulamento. Há uma ótima solução alternativa que você pode usar em algumas situações (acredito que a maioria) aqui: stackoverflow.com/questions/495021/…
Sheric
@Xupicor, você está certo. Tecnicamente, "Class Template" é o que você escreve para que possa instanciar uma "Classe Template" e seu objeto correspondente. No entanto, acredito que em uma terminologia genérica, usar ambos os termos alternadamente não seria tão errado, a sintaxe para definir o "Modelo de classe" em si começa com a palavra "modelo" e não "classe".
Sharjith N.
@Sheric, eu não disse que não há soluções alternativas. Na verdade, tudo o que está disponível são apenas soluções alternativas para simular a separação de interface e implementação no caso de classes de modelo. Nenhuma dessas soluções alternativas funciona sem instanciar uma classe de modelo digitada específica. De qualquer forma, isso dissolve todo o ponto de genericidade do uso de modelos de classe. Separar estruturas de dados de algoritmos não é o mesmo que separar estruturas de dados de métodos. As classes de estrutura de dados podem muito bem ter métodos como construtores, getters e setters.
Sharjith N.
A coisa mais próxima que descobri de fazer este trabalho é usar um par de arquivos .h / .hpp e #include "filename.hpp" no final do arquivo .h que define sua classe de modelo. (abaixo de sua chave de fechamento para a definição de classe com o ponto-e-vírgula). Isso, pelo menos estruturalmente, os separa do mesmo modo, e é permitido porque no final, o compilador copia / cola seu código .hpp sobre seu #include "filename.hpp".
Artorias2718
90

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

template class stack<int>;

Todos os métodos de pilha não-template serão instanciados e a etapa de vinculação funcionará bem.

Benoît
fonte
7
Na prática, a maioria das pessoas usa um arquivo cpp separado para isso - algo como stackinstantiations.cpp.
Nemanja Trifunovic
@NemanjaTrifunovic você pode dar um exemplo de como seria o stackinstantiations.cpp?
qwerty9967
3
Na verdade, existem outras soluções: codeproject.com/Articles/48575/…
sleepsort
@ Benoît Recebi um erro de erro: esperado unqualified-id antes de ';' pilha de modelo de token <int>; Você sabe por quê? Obrigado!
camino
3
Na verdade, a sintaxe correta é template class stack<int>;.
Paul Baltescu
8

Você pode fazer desta forma

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Isso foi discutido em Daniweb

Também no FAQ, mas usando a palavra-chave de exportação C ++.

Sadanand
fonte
5
includecppgeralmente é 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 uma hppou alguma extensão diferente (por exemplo tpp) para deixar bem claro o que está acontecendo, remover a confusão sobre makefiles direcionando arquivos reais cpp , etc.
sublinhado_d
@underscore_d Você poderia explicar por que incluir um .cpparquivo é uma ideia terrível?
Abbas
1
@Abbas porque a extensão cpp(ou cc, ou c, 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. #includeO processamento de um arquivo real cpp preencheria rapidamente sua tela com erros de definição múltipla, e com razão. neste caso, como não é uma razão para #includeisso, cppera apenas a escolha errada de extensão.
sublinhado_d
@underscore_d Então, basicamente, é errado apenas usar a .cppextensã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?
Abbas
1
@Abbas Sim, cpp/ cc/ etc deve ser evitada, mas é uma boa idéia para usar algo diferente hpp- por exemplo tpp, tcc, etc. - assim você pode reutilizar o resto do nome do arquivo e indicam que o tpparquivo, embora ele age como um cabeçalho, mantém a implementação fora de linha das declarações de modelo no correspondente hpp. 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 errada
sublinhado_d
6

Não, não é possível. Não sem a exportpalavra - 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.

Charles Salvia
fonte
3
Sua resposta não está correta. Você pode gerar código de uma classe de modelo em um arquivo cpp, desde que saiba quais argumentos de modelo usar. Veja minha resposta para mais informações.
Benoît
2
É verdade, mas isso vem com a séria restrição de precisar atualizar o arquivo .cpp e recompilar toda vez que um novo tipo que usa o modelo é introduzido, o que provavelmente não é o que o OP tinha em mente.
Charles Salvia
3

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.

Meteorhead
fonte
2

Somente se você #include "stack.cppno final de stack.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.

letra
fonte
4
Se estiver fazendo isso, você desejará adicionar #ifndef STACK_CPP (e amigos) ao seu arquivo stack.cpp.
Stephen Newell
Deixe-me levar por esta sugestão. Eu também não prefiro essa abordagem por motivos de estilo.
luke
2
Sim, nesse caso, o segundo arquivo definitivamente não deve receber a extensão cpp( ccou 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 uso tpppara isso, que com folga também pode ficar para tem ptarde im plementation (definições out-of-line). Divaguei mais sobre isso aqui: stackoverflow.com/questions/1724036/…
underscore_d
2

À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".

Konstantin Tenzin
fonte
+1 - embora não funcione muito bem na maioria das vezes (pelo menos, não com a frequência que eu gostaria)
peterchen
2

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

Macke
fonte
2

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:

#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;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}
Mark Ransom
fonte
8
Você não precisa da função fictícia: Use 'template stack <int>;' Isso força uma instanciação do modelo na unidade de compilação atual. Muito útil se você definir um modelo, mas quiser apenas algumas implementações específicas em uma biblioteca compartilhada.
Martin York
@Martin: incluindo todas as funções de membro? Isso é fantástico. Você deve adicionar esta sugestão ao tópico "recursos ocultos do C ++".
Mark Ransom
@LokiAstari Eu encontrei um artigo sobre isso caso alguém queira saber mais: cplusplus.com/forum/articles/14272
Andrew Larsson
1

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.

Aaron
fonte
0

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.

ChadNC
fonte
0

Outra possibilidade é fazer algo como:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

Não gosto dessa sugestão por uma questão de estilo, mas pode ser adequada para você.

Lucas
fonte
1
O segundo cabeçalho glorificado a ser incluído deve ter pelo menos uma extensão diferente cpppara evitar confusão com os arquivos de origem reais . As sugestões comuns incluem tppe tcc.
sublinhado_d
0

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

Shailesh Kumar
fonte
1
Esta é quase uma resposta apenas com link, e esse link está morto.
sublinhado_d
0

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:

        TClass<int> myObj;

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

Aaron01
fonte
0

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

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Resultado:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

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.

Sriram Murali
fonte
-3

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

Ahmad
fonte