Creación de instancias de plantilla explícita: ¿cuándo se utiliza?

Resuelto There is nothing we can do asked hace 14 años • 4 respuestas

Después de unas semanas de descanso, estoy tratando de ampliar y ampliar mis conocimientos sobre plantillas con el libro Plantillas: la guía completa de David Vandevoorde y Nicolai M. Josuttis, y lo que estoy tratando de entender en este momento es la creación de instancias explícitas de plantillas. .

En realidad, no tengo ningún problema con el mecanismo como tal, pero no puedo imaginar una situación en la que me gustaría o quisiera utilizar esta función. Si alguien me puede explicar eso estaré más que agradecido.

Aceptado

Si define una clase de plantilla, solo desea que funcione para un par de tipos explícitos.

Coloque la declaración de plantilla en el archivo de encabezado como una clase normal.

Coloque la definición de la plantilla en un archivo fuente como una clase normal.

Luego, al final del archivo fuente, cree explícitamente solo una instancia de la versión que desea que esté disponible.

Ejemplo tonto:

// 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;

Fuente:

// 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>;

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();
}
Martin York avatar Feb 28 '2010 16:02 Martin York

La creación de instancias explícitas permite reducir los tiempos de compilación y los tamaños de salida.

Estos son los principales beneficios que puede proporcionar. Provienen de los dos efectos siguientes que se describen en detalle en las secciones siguientes:

  • eliminar definiciones de los encabezados para evitar que los sistemas de compilación inteligentes reconstruyan los incluidos en cada cambio en esas plantillas (ahorra tiempo)
  • evitar la redefinición de objetos (ahorra tiempo y tamaño)

Eliminar definiciones de los encabezados

La creación de instancias explícitas le permite dejar definiciones en el archivo .cpp.

Cuando la definición está en el encabezado y usted la modifica, un sistema de compilación inteligente volvería a compilar todos los incluidos, que podrían ser docenas de archivos, lo que posiblemente haría que la recompilación incremental después de un solo cambio de archivo fuera insoportablemente lenta.

Poner definiciones en archivos .cpp tiene la desventaja de que las bibliotecas externas no pueden reutilizar la plantilla con sus propias clases nuevas, pero "Eliminar definiciones de los encabezados incluidos pero también exponer las plantillas como una API externa" a continuación muestra una solución alternativa.

Vea ejemplos concretos a continuación.

Ejemplos de sistemas de compilación que detectan inclusiones y reconstruyen:

  • CMake: Manejo de dependencias de archivos de encabezado con cmake
  • SCons: https://scons.org/doc/0.97/HTML/scons-man.html
  • Haga + algo de trabajo manual de GCC: genere dependencias para un archivo MAKE para un proyecto en C/C++

Beneficios de la redefinición de objetos: comprensión del problema

Si simplemente define completamente una plantilla en un archivo de encabezado, cada unidad de compilación que incluye ese encabezado termina compilando su propia copia implícita de la plantilla para cada uso diferente de argumento de plantilla realizado.

Esto significa mucho uso inútil del disco y tiempo de compilación.

Aquí hay un ejemplo concreto, en el que ambos main.cppse notmain.cppdefinen implícitamente MyTemplate<int>debido a su uso en esos archivos.

principal.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); }

miplantilla.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 ascendente .

Compile y vea símbolos con 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

Producción:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Entonces vemos que se genera una sección separada para cada instanciación de método y que cada uno de ellos ocupa, por supuesto, espacio en los archivos objeto.

En man nm, vemos que eso Wsignifica un símbolo débil, que GCC eligió porque es una función de plantilla.

La razón por la que no explota en el momento del enlace con múltiples definiciones es que el enlazador acepta múltiples definiciones débiles y simplemente elige una de ellas para colocarla en el ejecutable final, y todas son iguales en nuestro caso, por lo que todo es bien.

Los números en la salida significan:

  • 0000000000000000: dirección dentro de la sección. Este cero se debe a que las plantillas se colocan automáticamente en su propia sección.
  • 0000000000000017: tamaño del código generado para ellos

Esto lo podemos ver un poco más claro con:

objdump -S main.o | c++filt

que termina en:

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

y _ZN10MyTemplateIiE1fEies el nombre destrozado del MyTemplate<int>::f(int)>que c++filtdecidió no destrozar.

Soluciones al problema de la redefinición de objetos.

Este problema se puede evitar utilizando la creación de instancias explícita y:

  1. mantenga la definición en hpp y agregue extern templatehpp para los tipos de los que se crearán instancias explícitas.

    Como se explica en: el uso de una plantilla externa (C++11) extern template evita que las unidades de compilación creen instancias de una plantilla completamente definida, excepto nuestra creación de instancias explícita. De esta manera, sólo nuestra instanciación explícita se definirá en los objetos finales:

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

    miplantilla.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    principal.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); }
    

    Desventajas:

    • la definición permanece en el encabezado, lo que hace que el cambio de un solo archivo se recopile en ese encabezado posiblemente sea lento
    • Si tiene una biblioteca de solo encabezado, obligará a los proyectos externos a realizar su propia creación de instancias explícita. Si no tiene una biblioteca de solo encabezados, esta solución probablemente sea la mejor.
    • Si el tipo de plantilla está definido en su propio proyecto y no es un me gusta incorporado int, parece que se ve obligado a agregarle la inclusión en el encabezado, una declaración directa no es suficiente: plantilla externa y tipos incompletos. Esto aumenta las dependencias del encabezado. un poco.
  2. moviendo la definición en el archivo cpp, deje solo la declaración en hpp, es decir, modifique el ejemplo original para que sea:

    miplantilla.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    miplantilla.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    Downside: external projects can't use your template with their own types. Also you are forced to explicitly instantiate all types. But maybe this is an upside since then programmers won't forget.

  3. keep definition on hpp and add extern template on every includer:

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

    Downside: all includers have to add the extern to their CPP files, which programmers will likely forget to do.

With any of those solutions, nm now contains:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

so we see have only mytemplate.o has a compilation of MyTemplate<int> as desired, while notmain.o and main.o don't because U means undefined.

Remove definitions from included headers but also expose templates an external API in a header-only library

If your library is not header only, the extern template method will work, since using projects will just link to your object file, which will contain the object of the explicit template instantiation.

However, for header only libraries, if you want to both:

  • speed up your project's compilation
  • expose headers as an external library API for others to use it

then you can try one of the following:

    • mytemplate.hpp: template definition
    • mytemplate_interface.hpp: template declaration only matching the definitions from mytemplate_interface.hpp, no definitions
    • mytemplate.cpp: include mytemplate.hpp and make explicit instantitations
    • main.cpp and everywhere else in the code base: include mytemplate_interface.hpp, not mytemplate.hpp
    • mytemplate.hpp: template definition
    • mytemplate_implementation.hpp: includes mytemplate.hpp and adds extern to every class that will be instantiated
    • mytemplate.cpp: include mytemplate.hpp and make explicit instantitations
    • main.cpp and everywhere else in the code base: include mytemplate_implementation.hpp, not mytemplate.hpp

Or even better perhaps for multiple headers: create an intf/impl folder inside your includes/ folder and use mytemplate.hpp as the name always.

The mytemplate_interface.hpp approach looks like this:

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 and run:

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

Output:

2

Tested in Ubuntu 18.04.

C++20 modules

https://en.cppreference.com/w/cpp/language/modules

I think this feature will provide the best setup going forward as it becomes available, but I haven't checked it yet because it is not yet available on my GCC 9.2.1.

You will still have to do explicit instantiation to get the speedup/disk saving, but at least we will have a sane solution for "Remove definitions from included headers but also expose templates an external API" which does not require copying things around 100 times.

Expected usage (without the explicit insantiation, not sure what the exact syntax will be like, see: How to use template explicit instantiation with C++20 modules?) be something along:

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

and then compilation mentioned at 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

So from this we see that clang can extract the template interface + implementation into the magic helloworld.pcm, which must contain some LLVM intermediate representation of the source: How are templates handled in C++ module system? which still allows for template specification to happen.

How to quickly analyze your build to see if it would gain a lot from template instantiation

So, you've got a complex project and you want to decide if template instantiation will bring significant gains without actually doing the full refactor?

The analysis below might help you decide, or at least select the most promising objects to refactor first while you experiment, by borrowing some ideas from: My C++ object file is too big

# 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

The dream: a template compiler cache

I think the ultimate solution would be if we could build with:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

and then myfile.o would automatically reuse previously compiled templates across files.

This would mean 0 extra effort on the programmers besides passing that extra CLI option to your build system.

A secondary bonus of explicit template instantiation: help IDEs list template instantiations

I've found that some IDEs such as Eclipse cannot resolve "a list of all template instantiations used".

So e.g., if you are inside a templated code, and you want to find possible values of the template, you would have to find the constructor usages one by one and deduce the possible types one by one.

But on Eclipse 2020-03 I can easily list explicitly instantiated templates by doing a Find all usages (Ctrl + Alt + G) search on the class name, which points me e.g. from:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

to:

template class AnimalTemplate<Dog>;

Here's a demo: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Another guerrila technique you could use outside of the IDE however would be to run nm -C on the final executable and grep the template name:

nm -C main.out | grep AnimalTemplate

which directly points to the fact that Dog was one of the instantiations:

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