Armazenando definições de função de modelo C ++ em um arquivo .CPP

526

Eu tenho algum código de modelo que eu preferiria ter armazenado em um arquivo CPP em vez de embutido no cabeçalho. Eu sei que isso pode ser feito desde que você saiba quais tipos de modelo serão usados. Por exemplo:

arquivo .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

arquivo .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Observe as duas últimas linhas - a função de modelo foo :: do é usada apenas com ints e std :: strings; portanto, essas definições significam que o aplicativo será vinculado.

Minha pergunta é - isso é um truque desagradável ou funcionará com outros compiladores / linkers? No momento, estou usando esse código apenas com o VS2008, mas desejarei portar para outros ambientes.

Roubar
fonte
22
Eu não fazia ideia de que isso era possível - um truque interessante! Teria ajudado algumas tarefas recentes consideráveis ​​saber isso - um brinde!
xan
69
A única coisa que me pisa é o uso de docomo um identificador: p
Quentin
eu fiz algo parecido com o gcc, mas ainda estou pesquisando
Nick
16
Este não é um "hack", é uma decleração para a frente. Isso tem um lugar no padrão da língua; então sim, é permitido em todos os compiladores conformes padrão.
Ahmet Ipkin 7/03/16
1
E se você tiver dezenas de métodos? Você pode apenas fazer template class foo<int>;template class foo<std::string>;no final do arquivo .cpp?
Ignorant

Respostas:

231

O problema que você descreve pode ser resolvido definindo o modelo no cabeçalho ou através da abordagem descrita acima.

Eu recomendo a leitura dos seguintes pontos do C ++ FAQ Lite :

Eles entram em muitos detalhes sobre esses (e outros) problemas de modelo.

Aaron N. Tubbs
fonte
39
Apenas para complementar a resposta, o link referenciado responde positivamente à pergunta, ou seja, é possível fazer o que Rob sugeriu e ter o código portátil.
Ivotron
161
Você pode apenas postar as partes relevantes na própria resposta? Por que essa referência é permitida no SO. Não tenho idéia do que procurar neste link, pois ele foi fortemente alterado desde então.
Ident
124

Para outros nesta página perguntando qual é a sintaxe correta (como eu) para a especialização explícita de modelos (ou pelo menos no VS2008), é a seguinte ...

No seu arquivo .h ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

E no seu arquivo .cpp

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
espaço para nome sid
fonte
15
Você quer dizer "para especialização explícita do modelo CLASS". Nesse caso, isso cobrirá todas as funções que a classe de modelo tem?
Arthur
@ Arthur não parece, eu tenho alguns métodos de modelo permanecer no cabeçalho e a maioria dos outros métodos no cpp, funciona bem. Solução muito boa.
user1633272
No caso do solicitante, eles têm um modelo de função, não um modelo de classe.
user253751 6/03
23

Este código está bem formado. Você só precisa prestar atenção que a definição do modelo é visível no momento da instanciação. Para citar o padrão, § 14.7.2.4:

A definição de um modelo de função não exportado, de um modelo de função de membro não exportado ou de uma função de membro não exportada ou de um membro de dados estáticos de um modelo de classe deve estar presente em todas as unidades de conversão em que é explicitamente instanciado.

Konrad Rudolph
fonte
2
O que significa não exportado ?
Dan Nissenbaum
1
@ Dan Visível apenas dentro de sua unidade de compilação, e não fora dela. Se você vincular várias unidades de compilação, os símbolos exportados poderão ser usados ​​entre eles (e devem ter um único, ou pelo menos, no caso de modelos, definições consistentes; caso contrário, você encontrará o UB).
Konrad Rudolph
Obrigado. Eu pensei que todas as funções são (por padrão) visíveis fora da unidade de compilação. Se eu tiver duas unidades de compilação a.cpp(definindo a função a() {}) e b.cpp(definindo a função b() { a() }), isso será vinculado com êxito. Se eu estiver certo, a citação acima parece não se aplicar ao caso típico ... estou errado em algum lugar?
Dan Nissenbaum 12/06
@Dan Trivial counterexample: inlinefunctions
Konrad Rudolph
1
Os modelos da função @Dan são implicitamente inline. A razão é que, sem uma ABI C ++ padronizada, é difícil / impossível definir o efeito que isso teria.
Konrad Rudolph
15

Isso deve funcionar bem em todos os lugares onde os modelos são suportados. A instanciação explícita do modelo faz parte do padrão C ++.

sombra da Lua
fonte
13

Seu exemplo está correto, mas não é muito portátil. Também há uma sintaxe um pouco mais limpa que pode ser usada (como apontado por @ namespace-sid).

Suponha que a classe modelada faça parte de alguma biblioteca que deve ser compartilhada. Outras versões da classe de modelo devem ser compiladas? O mantenedor da biblioteca deve antecipar todos os possíveis usos de modelo da classe?

Uma abordagem alternativa é uma pequena variação do que você possui: adicione um terceiro arquivo que é o arquivo de implementação / instanciação do modelo.

arquivo foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

arquivo foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

arquivo foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

A única ressalva é que você precisa dizer ao compilador para compilar em foo-impl.cppvez de foo.cppcompilar o último não faz nada.

Obviamente, você pode ter várias implementações no terceiro arquivo ou vários arquivos de implementação para cada tipo que você deseja usar.

Isso permite muito mais flexibilidade ao compartilhar a classe de modelo para outros usos.

Essa configuração também reduz o tempo de compilação para classes reutilizadas, porque você não está recompilando o mesmo arquivo de cabeçalho em cada unidade de tradução.

Cameron Tacklind
fonte
o que isso te compra? Você ainda precisa editar foo-impl.cpp para adicionar uma nova especialização.
MK.
Separação dos detalhes de implementação (também conhecidos como definições em foo.cpp) das quais as versões são realmente compiladas (in foo-impl.cpp) e declarações (in foo.h). Não gosto que a maioria dos modelos de C ++ seja definida inteiramente em arquivos de cabeçalho. Isso é contrário ao padrão C / C ++ de pares c[pp]/hpara cada classe / namespace / qualquer agrupamento que você usar. As pessoas parecem ainda usar arquivos de cabeçalho monolíticos simplesmente porque essa alternativa não é amplamente usada ou conhecida.
Cameron Tacklind 29/03/19
1
@MK. Eu estava colocando as instanciações explícitas do modelo no final da definição no arquivo de origem primeiro, até precisar de instâncias adicionais em outro lugar (por exemplo, testes de unidade usando um mock como o tipo de modelo). Essa separação me permite adicionar mais instanciações externamente. Além disso, ele ainda funciona quando eu mantenho o original como um h/cpppar, embora eu tenha que cercar a lista original de instanciações em uma proteção de inclusão, mas ainda assim eu poderia compilar o foo.cppnormal. Ainda sou bastante novo em C ++ e gostaria de saber se esse uso misto tem alguma ressalva adicional.
Thirdwater
3
Eu acho que é preferível desacoplar foo.cppe foo-impl.cpp. Não #include "foo.cpp"no foo-impl.cpparquivo; em vez disso, adicione a declaração extern template class foo<int>;para foo.cppimpedir que o compilador instancie o modelo ao compilar foo.cpp. Assegure-se de que o sistema de construção crie os dois .cpparquivos e transmita os dois arquivos de objeto ao vinculador. Isso tem vários benefícios: a) é claro foo.cppque não há instanciação; b) as alterações no foo.cpp não exigem uma recompilação do foo-impl.cpp.
Shmuel Levine
3
Essa é uma abordagem muito boa para o problema das definições de modelos que leva o melhor dos dois mundos - implementação e instanciação de cabeçalho para tipos usados ​​com freqüência. A única mudança que eu faria para esta configuração é para mudar o nome foo.cppem foo_impl.he foo-impl.cppem apenas foo.cpp. Eu também adicionaria typedefs para instanciações de foo.cpppara foo.h, da mesma forma using foo_int = foo<int>;. O truque é fornecer aos usuários duas interfaces de cabeçalho para uma escolha. Quando o usuário precisa de instanciação predefinida, ele inclui foo.h, quando o usuário precisa de algo fora de ordem, ele inclui foo_impl.h.
Wormer
5

Definitivamente, este não é um truque desagradável, mas esteja ciente do fato de que você precisará fazer isso (a especialização explícita do modelo) para cada classe / tipo que você deseja usar com o modelo fornecido. No caso de MUITOS tipos solicitando instanciação de modelo, pode haver MUITAS linhas no seu arquivo .cpp. Para solucionar esse problema, você pode ter um TemplateClassInst.cpp em cada projeto usado, para ter maior controle sobre os tipos que serão instanciados. Obviamente, essa solução não será perfeita (também conhecida como bala de prata), pois você pode acabar quebrando o ODR :).

XIII vermelho
fonte
Você tem certeza de que isso quebrará o ODR? Se as linhas de instanciação no TemplateClassInst.cpp se referirem ao arquivo de origem idêntico (contendo as definições de função do modelo), não é garantido que não viole o ODR, pois todas as definições são idênticas (mesmo se repetidas)?
Dan Nissenbaum 12/06
Por favor, o que é ODR?
não removível
4

Existe, no padrão mais recente, uma palavra-chave (export ) que ajudaria a aliviar esse problema, mas não foi implementada em nenhum compilador que eu conheça, além do Comeau.

Veja o FAQ-lite sobre isso.

Ben Collins
fonte
2
AFAIK, a exportação está inoperante porque eles estão enfrentando problemas cada vez mais recentes, sempre que resolvem o último, tornando a solução geral cada vez mais complicada. E a palavra-chave "export" não permitirá que você "exporte" de um CPP de qualquer maneira (ainda de H. Sutter). Então eu digo: Não prenda a respiração ...
paercebal
2
Para implementar a exportação, o compilador ainda requer a definição completa do modelo. Tudo o que você ganha é tê-lo em uma forma de compilação. Mas, na verdade, não faz sentido.
Zan Lynx 31/03
2
... e saiu do padrão, devido a complicações excessivas para ganho mínimo.
DevSolar 26/08/15
4

Essa é uma maneira padrão de definir funções de modelo. Eu acho que existem três métodos que li para definir modelos. Ou provavelmente 4. Cada um com prós e contras.

  1. Defina na definição de classe. Não gosto nada disso, porque acho que as definições de classe são estritamente para referência e devem ser fáceis de ler. No entanto, é muito menos complicado definir modelos na classe do que fora. E nem todas as declarações de modelo estão no mesmo nível de complexidade. Esse método também torna o modelo um modelo verdadeiro.

  2. Defina o modelo no mesmo cabeçalho, mas fora da classe. Esta é a minha maneira preferida na maioria das vezes. Ele mantém sua definição de classe organizada, o modelo permanece um modelo verdadeiro. No entanto, requer nomeação de modelo completo, o que pode ser complicado. Além disso, seu código está disponível para todos. Mas se você precisar que seu código esteja embutido, esse é o único caminho. Você também pode fazer isso criando um arquivo .INL no final das suas definições de classe.

  3. Inclua o header.he Implementation.CPP no seu main.CPP. Eu acho que é assim que é feito. Você não precisará preparar nenhuma pré-instanciação, ela se comportará como um verdadeiro modelo. O problema que tenho com isso é que não é natural. Normalmente, não incluímos e esperamos incluir arquivos de origem. Eu acho que desde que você incluiu o arquivo de origem, as funções do modelo podem ser incorporadas.

  4. Este último método, que foi publicado, está definindo os modelos em um arquivo de origem, assim como o número 3; mas, em vez de incluir o arquivo de origem, instanciamos os modelos para os que precisaremos. Não tenho nenhum problema com esse método e às vezes é útil. Temos um código grande, que não pode se beneficiar da inclusão, basta colocá-lo em um arquivo CPP. E se conhecemos instanciações comuns e podemos predefini-las. Isso nos impede de escrever basicamente a mesma coisa 5, 10 vezes. Este método tem o benefício de manter nosso código proprietário. Mas não recomendo colocar pequenas funções usadas regularmente em arquivos CPP. Como isso reduzirá o desempenho da sua biblioteca.

Observe que não estou ciente das conseqüências de um arquivo obj inchado.

Cássio Renan
fonte
3

Sim, essa é a maneira padrão de se especializar instanciação explícita de . Como você afirmou, não é possível instanciar este modelo com outros tipos.

Editar: corrigido com base no comentário.

Lou Franco
fonte
Ser exigente quanto à terminologia é uma "instanciação explícita".
Richard Corden
2

Vamos dar um exemplo, digamos que, por algum motivo, você queira ter uma classe de modelo:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Se você compilar esse código com o Visual Studio, ele funcionará imediatamente. O gcc produzirá um erro de vinculador (se o mesmo arquivo de cabeçalho for usado em vários arquivos .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

É possível mover a implementação para o arquivo .cpp, mas você precisa declarar uma classe como esta -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

E então o .cpp ficará assim:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Sem duas últimas linhas no arquivo de cabeçalho - o gcc funcionará bem, mas o Visual studio produzirá um erro:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

A sintaxe da classe de modelo é opcional, caso você queira expor a função via exportação .dll, mas isso é aplicável apenas à plataforma Windows - para que test_template.h possa se parecer com o seguinte:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

com o arquivo .cpp do exemplo anterior.

No entanto, isso dá mais dor de cabeça ao vinculador, por isso é recomendável usar o exemplo anterior se você não exportar a função .dll.

TarmoPikaro
fonte
1

Hora de uma atualização! Crie um arquivo embutido (.inl ou provavelmente qualquer outro) e simplesmente copie todas as suas definições nele. Certifique-se de adicionar o modelo acima de cada função ( template <typename T, ...>). Agora, em vez de incluir o arquivo de cabeçalho no arquivo embutido, faça o contrário. Inclua o arquivo embutido após a declaração da sua classe (#include "file.inl" ).

Realmente não sei por que ninguém mencionou isso. Não vejo desvantagens imediatas.

Didii
fonte
25
A desvantagem imediata é que é fundamentalmente o mesmo que apenas definir as funções do modelo diretamente no cabeçalho. Uma vez que você #include "file.inl", o pré-processador cole o conteúdo file.inldiretamente no cabeçalho. Qualquer que seja o motivo que você queira evitar que a implementação entre no cabeçalho, esta solução não resolve esse problema.
Cody Gray
5
- e significa que você, tecnicamente desnecessariamente, se sobrecarrega com a tarefa de escrever todo o padrão detalhado e alucinante necessário para templatedefinições fora de linha . Entendo por que as pessoas querem fazer isso - para obter a maior paridade com declarações / definições que não são do modelo, para manter a declaração da interface arrumada etc. -, mas nem sempre vale a pena. É um caso de avaliar os trade-offs de ambos os lados e escolher o menos ruim . ... até que namespace classse torna uma coisa: O [ por favor, seja uma coisa ]
underscore_d
2
@ Andrew Parece ter ficado preso nos canos do Comitê, embora eu acho que vi alguém dizendo que não era intencional. Eu gostaria que tivesse entrado em C ++ 17. Talvez na próxima década.
Underscore_d
@CodyGray: Tecnicamente, isso é realmente o mesmo para o compilador e, portanto, não está reduzindo o tempo de compilação. Ainda acho que vale a pena mencionar e praticar em vários projetos que já vi. Seguir esse caminho ajuda a separar a Interface da definição, o que é uma boa prática. Nesse caso, não ajuda na compatibilidade com a ABI ou similar, mas facilita a leitura e a compreensão da interface.
Kiloalphaindia
0

Não há nada errado com o exemplo que você deu. Mas devo dizer que acredito que não é eficiente armazenar definições de funções em um arquivo cpp. Entendo apenas a necessidade de separar a declaração e a definição da função.

Quando usada em conjunto com instanciação explícita de classe, a BCCL (Boost Concept Check Library) pode ajudá-lo a gerar o código de função do modelo nos arquivos cpp.

Benoît
fonte
8
O que é ineficiente sobre isso?
Cody Gray
0

Nenhuma das opções acima funcionou para mim, então aqui está como você resolveu, minha classe tem apenas 1 método modelado ..

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

isso evita erros do vinculador e não é necessário chamar TemporaryFunction

KronuZ
fonte