Instanciação explícita do modelo - quando é usado?


102

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.

Respostas:


68

Copiado diretamente de https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

Você pode usar a instanciação explícita para criar uma instanciação de uma classe ou função com modelo sem realmente usá-la em seu código. Como isso é útil ao criar arquivos de biblioteca (.lib) que usam modelos para distribuição, as definições de modelo não instanciadas não são colocadas em arquivos de objeto (.obj).

(Por exemplo, libstdc ++ contém a instanciação explícita de std::basic_string<char,char_traits<char>,allocator<char> >(que é std::string), portanto, toda vez que você usa funções de std::string, o mesmo código de função não precisa ser copiado para objetos. O compilador só precisa referir-se (vincular) a libstdc ++.)


8
Sim, as bibliotecas MSVC CRT têm instanciações explícitas para todas as classes de stream, locale e string, especializadas para char e wchar_t. O .lib resultante tem mais de 5 megabytes.
Hans Passant

4
Como o compilador sabe que o modelo foi explicitamente instanciado em outro lugar? Não vai apenas gerar a definição de classe porque está disponível?

@STing: Se o modelo for instanciado, haverá uma entrada dessas funções na tabela de símbolos.
kennytm

@Kenny: Você quer dizer se já foi instanciado na mesma TU? Eu presumiria que qualquer compilador é inteligente o suficiente para não instanciar a mesma especialização mais de uma vez na mesma TU. Achei que o benefício da instanciação explícita (com relação aos tempos de construção / link) é que se uma especialização for (explicitamente) instanciada em uma TU, ela não será instanciada nas outras TUs nas quais é usada. Não?

4
@Kenny: Eu sei sobre a opção do GCC para evitar a instanciação implícita, mas este não é um padrão. Pelo que eu sei, o VC ++ não tem essa opção. Inst. Explícita é sempre apontado como melhorando os tempos de compilação / link (até mesmo por Bjarne), mas para que sirva a esse propósito, o compilador deve de alguma forma saber que não instancia modelos implicitamente (por exemplo, por meio do sinalizador GCC), ou não deve receber o definição de modelo, apenas uma declaração. Isso soa correto? Estou apenas tentando entender por que alguém usaria a instanciação explícita (além de limitar os tipos concretos).

89

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();
}

1
É correto dizer que se o compilador tiver a definição de modelo inteira (incluindo definições de função) em uma determinada unidade de tradução, ele irá instanciar uma especialização do modelo quando necessário (independentemente de essa especialização ter sido explicitamente instanciada em outra TU)? Ou seja, para colher os benefícios do tempo de compilação / link da instanciação explícita, deve-se incluir apenas a declaração do modelo para que o compilador não possa instanciá-la.

1
@ user123456: Provavelmente dependente do compilador. Porém, provavelmente é verdade na maioria das situações.
Martin York

1
existe uma maneira de fazer o compilador usar esta versão explicitamente instanciada para os tipos que você pré-especificou, mas se você tentar instanciar o modelo com um tipo "estranho / inesperado", faça com que funcione "normalmente", onde apenas instancia o modelo conforme necessário?
David Doria

2
o que seria uma boa verificação / teste para ter certeza de que as instanciações explícitas estão realmente sendo usadas? Ou seja, está funcionando, mas não estou totalmente convencido de que não é apenas instanciar todos os modelos sob demanda.
David Doria

7
A maior parte da conversa de comentário acima não é mais verdadeira, pois c ++ 11: Uma declaração de instanciação explícita (um modelo externo) evita instanciações implícitas: o código que de outra forma causaria uma instanciação implícita deve usar a definição de instanciação explícita fornecida em outro lugar no programa (normalmente, em outro arquivo: isso pode ser usado para reduzir os tempos de compilação) en.cppreference.com/w/cpp/language/class_template
xaxxon

30

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 para evitar que as ferramentas de construção reconstruam os includers (economiza tempo)
  • redefinição de objeto (economiza tempo e tamanho)

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.cppe notmain.cppimplicitamente definem MyTemplate<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 que Wsignifica 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ções
  • 0000000000000017: tamanho do código gerado para eles

Podemos ver isso um pouco mais claramente com:

objdump -S main.o | c++filt

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 do MyTemplate<int>::f(int)>qual c++filtdecidiu 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 templatehpp 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:

    • a definição permanece no cabeçalho, tornando a recompilação de alteração de arquivo único para esse cabeçalho possivelmente lenta
    • se você for uma biblioteca apenas de cabeçalho, você força os projetos externos a fazerem sua própria instanciação explícita. Se você não for uma biblioteca apenas de cabeçalho, esta solução é provavelmente a melhor.
    • se o tipo de modelo é definido em seu próprio projeto e não em um embutido 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 templateem 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 externaos seus arquivos CPP, o que os programadores provavelmente se esquecerão de fazer.

Com qualquer uma dessas soluções, nmagora 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.ouma compilação de MyTemplate<int>como desejado, enquanto notmain.oe main.onão porque Usignifica 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 templatemé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:

  • acelerar a compilação do seu projeto
  • expor cabeçalhos como uma API de biblioteca externa para outros usarem

então você pode tentar um dos seguintes:

    • mytemplate.hpp: definição de modelo
    • mytemplate_interface.hpp: declaração do modelo apenas correspondendo às definições de mytemplate_interface.hpp, sem definições
    • mytemplate.cpp: inclui mytemplate.hppe faz instanciações explícitas
    • main.cppe em qualquer outro lugar na base de código: include mytemplate_interface.hpp, notmytemplate.hpp
    • mytemplate.hpp: definição de modelo
    • mytemplate_implementation.hpp: inclui mytemplate.hppe adiciona externa cada classe que será instanciada
    • mytemplate.cpp: inclui mytemplate.hppe faz instanciações explícitas
    • main.cppe em qualquer outro lugar na base de código: include mytemplate_implementation.hpp, notmytemplate.hpp

Ou melhor ainda, para cabeçalhos múltiplos: crie uma pasta intf/ impldentro de sua includes/pasta e use mytemplate.hppsempre como o nome.

A mytemplate_interface.hppabordagem é 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.oreutilizaria 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 -Cno executável final e executar o grep no nome do modelo:

nm -C main.out | grep AnimalTemplate

que aponta diretamente para o fato de que Dogfoi uma das instanciações:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.