Depois de algumas semanas de intervalo, estou tentando expandir e estender meu conhecimento sobre modelos com o livro Templates - The Complete Guide de David Vandevoorde e Nicolai M. Josuttis, e o que estou tentando entender neste momento é a instanciação explícita de modelos .
Na verdade, não tenho nenhum problema com o mecanismo como tal, mas não consigo imaginar uma situação em que eu gostaria ou queira usar esse recurso. Se alguém puder explicar isso para mim, ficarei mais do que grato.
Se você definir uma classe de modelo que deseja que funcione apenas para alguns tipos explícitos.
Coloque a declaração do modelo no arquivo de cabeçalho como uma classe normal.
Coloque a definição do modelo em um arquivo de origem, como uma classe normal.
Em seguida, no final do arquivo de origem, instancie explicitamente apenas a versão que você deseja disponibilizar.
Exemplo bobo:
// StringAdapter.h template<typename T> class StringAdapter { public: StringAdapter(T* data); void doAdapterStuff(); private: std::basic_string<T> m_data; }; typedef StringAdapter<char> StrAdapter; typedef StringAdapter<wchar_t> WStrAdapter;
Fonte:
// StringAdapter.cpp #include "StringAdapter.h" template<typename T> StringAdapter<T>::StringAdapter(T* data) :m_data(data) {} template<typename T> void StringAdapter<T>::doAdapterStuff() { /* Manipulate a string */ } // Explicitly instantiate only the classes you want to be defined. // In this case I only want the template to work with characters but // I want to support both char and wchar_t with the same code. template class StringAdapter<char>; template class StringAdapter<wchar_t>;
a Principal
#include "StringAdapter.h" // Note: Main can not see the definition of the template from here (just the declaration) // So it relies on the explicit instantiation to make sure it links. int main() { StrAdapter x("hi There"); x.doAdapterStuff(); }
fonte
A instanciação explícita permite reduzir os tempos de compilação e os tamanhos dos objetos
Esses são os principais ganhos que ela pode proporcionar. Eles vêm dos dois efeitos a seguir descritos em detalhes nas seções abaixo:
Remova as definições dos cabeçalhos
A instanciação explícita permite que você deixe definições no arquivo .cpp.
Quando a definição está no cabeçalho e você a modifica, um sistema de compilação inteligente recompila todos os includers, que podem ser dezenas de arquivos, possivelmente tornando a recompilação incremental após uma única alteração de arquivo insuportavelmente lenta.
Colocar definições em arquivos .cpp tem a desvantagem de que as bibliotecas externas não podem reutilizar o modelo com suas próprias novas classes, mas "Remova as definições dos cabeçalhos incluídos, mas também exponha os modelos de uma API externa" abaixo mostra uma solução alternativa.
Veja exemplos concretos abaixo.
Ganhos de redefinição de objeto: entendendo o problema
Se você apenas definir completamente um modelo em um arquivo de cabeçalho, cada unidade de compilação que inclui esse cabeçalho termina compilando sua própria cópia implícita do modelo para cada uso de argumento de modelo diferente feito.
Isso significa muito uso de disco e tempo de compilação inúteis.
Aqui está um exemplo concreto, em que ambos
main.cpp
enotmain.cpp
implicitamente definemMyTemplate<int>
devido ao seu uso nesses arquivos.main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" int notmain() { return MyTemplate<int>().f(1); }
mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t) { return t + 1; } }; #endif
notmain.hpp
#ifndef NOTMAIN_HPP #define NOTMAIN_HPP int notmain(); #endif
GitHub upstream .
Compile e veja os símbolos com
nm
:g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o echo notmain.o nm -C -S notmain.o | grep MyTemplate echo main.o nm -C -S main.o | grep MyTemplate
Resultado:
notmain.o 0000000000000000 0000000000000017 W MyTemplate<int>::f(int) main.o 0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
De
man nm
, vemos queW
significa símbolo fraco, que o GCC escolheu porque esta é uma função de modelo.O motivo pelo qual ele não explode no momento do link com várias definições é que o vinculador aceita várias definições fracas e apenas escolhe uma delas para colocar no executável final, e todas são iguais em nosso caso, então tudo é bem.
Os números na saída significam:
0000000000000000
: endereço dentro da seção. Esse zero ocorre porque os modelos são colocados automaticamente em suas próprias seções0000000000000017
: tamanho do código gerado para elesPodemos ver isso um pouco mais claramente com:
que termina em:
Disassembly of section .text._ZN10MyTemplateIiE1fEi: 0000000000000000 <MyTemplate<int>::f(int)>: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 89 7d f8 mov %rdi,-0x8(%rbp) c: 89 75 f4 mov %esi,-0xc(%rbp) f: 8b 45 f4 mov -0xc(%rbp),%eax 12: 83 c0 01 add $0x1,%eax 15: 5d pop %rbp 16: c3 retq
e
_ZN10MyTemplateIiE1fEi
é o nome mutilado doMyTemplate<int>::f(int)>
qualc++filt
decidiu não desfazer.Portanto, vemos que uma seção separada é gerada para cada instanciação de método e que cada um deles ocupa, é claro, espaço nos arquivos de objeto.
Soluções para o problema de redefinição de objeto
Esse problema pode ser evitado usando instanciação explícita e:
mantenha a definição em hpp e adicione
extern template
hpp para tipos que serão explicitamente instanciados.Conforme explicado em: usar extern template (C ++ 11)
extern template
evita que um template completamente definido seja instanciado por unidades de compilação, exceto para nossa instanciação explícita. Dessa forma, apenas nossa instanciação explícita será definida nos objetos finais:mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t) { return t + 1; } }; extern template class MyTemplate<int>; #endif
mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation required just for int. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" int notmain() { return MyTemplate<int>().f(1); }
Desvantagens:
int
, parece que você é forçado a adicionar a inclusão para ele no cabeçalho, uma declaração direta não é suficiente: modelo externo e tipos incompletos Isso aumenta as dependências do cabeçalho um pouco.movendo a definição no arquivo cpp, deixe apenas a declaração no hpp, ou seja, modifique o exemplo original para ser:
mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP template<class T> struct MyTemplate { T f(T t); }; #endif
mytemplate.cpp
#include "mytemplate.hpp" template<class T> T MyTemplate<T>::f(T t) { return t + 1; } // Explicit instantiation. template class MyTemplate<int>;
Desvantagem: projetos externos não podem usar seu modelo com seus próprios tipos. Além disso, você é forçado a instanciar explicitamente todos os tipos. Mas talvez isso seja uma vantagem, já que os programadores não esquecerão.
mantenha a definição no hpp e adicione
extern template
em todos os incluídos:mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate.hpp" #include "notmain.hpp" // extern template declaration extern template class MyTemplate<int>; int main() { std::cout << notmain() + MyTemplate<int>().f(1) << std::endl; }
notmain.cpp
#include "mytemplate.hpp" #include "notmain.hpp" // extern template declaration extern template class MyTemplate<int>; int notmain() { return MyTemplate<int>().f(1); }
Desvantagem: todos os incluídos precisam adicionar o
extern
aos seus arquivos CPP, o que os programadores provavelmente se esquecerão de fazer.Com qualquer uma dessas soluções,
nm
agora contém:notmain.o U MyTemplate<int>::f(int) main.o U MyTemplate<int>::f(int) mytemplate.o 0000000000000000 W MyTemplate<int>::f(int)
então vemos que tem apenas
mytemplate.o
uma compilação deMyTemplate<int>
como desejado, enquantonotmain.o
emain.o
não porqueU
significa indefinido.Remova as definições dos cabeçalhos incluídos, mas também exponha os modelos de uma API externa em uma biblioteca somente de cabeçalho
Se a sua biblioteca não for apenas cabeçalho, o
extern template
método funcionará, já que o uso de projetos irá apenas vincular ao seu arquivo objeto, que conterá o objeto da instanciação explícita do template.No entanto, para bibliotecas apenas de cabeçalho, se você quiser:
então você pode tentar um dos seguintes:
mytemplate.hpp
: definição de modelomytemplate_interface.hpp
: declaração do modelo apenas correspondendo às definições demytemplate_interface.hpp
, sem definiçõesmytemplate.cpp
: incluimytemplate.hpp
e faz instanciações explícitasmain.cpp
e em qualquer outro lugar na base de código: includemytemplate_interface.hpp
, notmytemplate.hpp
mytemplate.hpp
: definição de modelomytemplate_implementation.hpp
: incluimytemplate.hpp
e adicionaextern
a cada classe que será instanciadamytemplate.cpp
: incluimytemplate.hpp
e faz instanciações explícitasmain.cpp
e em qualquer outro lugar na base de código: includemytemplate_implementation.hpp
, notmytemplate.hpp
Ou melhor ainda, para cabeçalhos múltiplos: crie uma pasta
intf
/impl
dentro de suaincludes/
pasta e usemytemplate.hpp
sempre como o nome.A
mytemplate_interface.hpp
abordagem é assim:mytemplate.hpp
#ifndef MYTEMPLATE_HPP #define MYTEMPLATE_HPP #include "mytemplate_interface.hpp" template<class T> T MyTemplate<T>::f(T t) { return t + 1; } #endif
mytemplate_interface.hpp
#ifndef MYTEMPLATE_INTERFACE_HPP #define MYTEMPLATE_INTERFACE_HPP template<class T> struct MyTemplate { T f(T t); }; #endif
mytemplate.cpp
#include "mytemplate.hpp" // Explicit instantiation. template class MyTemplate<int>;
main.cpp
#include <iostream> #include "mytemplate_interface.hpp" int main() { std::cout << MyTemplate<int>().f(1) << std::endl; }
Compile e execute:
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp g++ -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o
Resultado:
2
Testado no Ubuntu 18.04.
Módulos C ++ 20
https://en.cppreference.com/w/cpp/language/modules
Acho que esse recurso fornecerá a melhor configuração no futuro, à medida que estiver disponível, mas ainda não o verifiquei porque ainda não está disponível em meu GCC 9.2.1.
Você ainda terá que fazer uma instanciação explícita para obter a aceleração / economia de disco, mas pelo menos teremos uma solução sensata para "Remover definições de cabeçalhos incluídos, mas também expor modelos de API externa" que não exige a cópia de cerca de 100 vezes.
O uso esperado (sem o insantiation explícito, não tenho certeza de como será a sintaxe exata, consulte: Como usar a instanciação explícita de modelo com módulos C ++ 20? ) Seja algo ao longo:
helloworld.cpp
export module helloworld; // module declaration import <iostream>; // import declaration template<class T> export void hello(T t) { // export declaration std::cout << t << std::end; }
main.cpp
import helloworld; // import declaration int main() { hello(1); hello("world"); }
e a compilação mencionada em https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/
clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm clang++ -std=c++2a -c -o helloworld.o helloworld.cpp clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o
Portanto, vemos que o clang pode extrair a interface + implementação do template na mágica
helloworld.pcm
, que deve conter alguma representação LLVM intermediária da fonte: Como os templates são tratados no sistema de módulo C ++? que ainda permite que a especificação do modelo aconteça.Como analisar rapidamente sua construção para ver se ela ganharia muito com a instanciação do modelo
Então, você tem um projeto complexo e quer decidir se a instanciação do template trará ganhos significativos sem realmente fazer a refatoração completa?
A análise abaixo pode ajudá-lo a decidir, ou pelo menos selecionar os objetos mais promissores para refatorar primeiro enquanto você experimenta, pegando emprestadas algumas idéias de: Meu arquivo de objeto C ++ é muito grande
# List all weak symbols with size only, no address. find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' | grep ' W ' > nm.log # Sort by symbol size. sort -k1 -n nm.log -o nm.sort.log # Get a repetition count. uniq -c nm.sort.log > nm.uniq.log # Find the most repeated/largest objects. sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log # Find the objects that would give you the most gain after refactor. # This gain is calculated as "(n_occurences - 1) * size" which is # the size you would gain for keeping just a single instance. # If you are going to refactor anything, you should start with the ones # at the bottom of this list. awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log | sort -k1 -n > nm.gains.log # Total gain if you refactored everything. awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log # Total size. The closer total gain above is to total size, the more # you would gain from the refactor. awk 'START{sum=0}{sum += $1}END{print sum}' nm.log
O sonho: um cache de compilador de template
Acho que a solução final seria se pudéssemos construir com:
g++ --template-cache myfile.o file1.cpp g++ --template-cache myfile.o file2.cpp
e então
myfile.o
reutilizaria automaticamente os modelos previamente compilados nos arquivos.Isso significaria 0 esforço extra para os programadores, além de passar essa opção CLI extra para seu sistema de construção.
Um bônus secundário de instanciação de modelo explícito: ajuda IDEs listar instanciações de modelo
Descobri que alguns IDEs como o Eclipse não podem resolver "uma lista de todas as instanciações de modelo usadas".
Então, por exemplo, se você estiver dentro de um código de modelo e quiser encontrar os valores possíveis do modelo, terá que encontrar os usos do construtor um por um e deduzir os tipos possíveis um por um.
Mas no Eclipse 2020-03, posso facilmente listar modelos explicitamente instanciados fazendo uma pesquisa Localizar todos os usos (Ctrl + Alt + G) no nome da classe, que me aponta, por exemplo:
template <class T> struct AnimalTemplate { T animal; AnimalTemplate(T animal) : animal(animal) {} std::string noise() { return animal.noise(); } };
para:
template class AnimalTemplate<Dog>;
Aqui está uma demonstração: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15
Outra técnica de guerrilha que você pode usar fora do IDE, no entanto, seria executar
nm -C
no executável final e executar o grep no nome do modelo:que aponta diretamente para o fato de que
Dog
foi uma das instanciações:0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]() 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog) 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
fonte
Depende do modelo do compilador - aparentemente existe o modelo Borland e o modelo CFront. E então também depende da sua intenção - se você está escrevendo uma biblioteca, você pode (como aludido acima) explicitamente instanciar as especializações que deseja.
A página GNU c ++ discute os modelos aqui https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .
fonte