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


95

Después de un descanso de algunas semanas, estoy tratando de expandir y ampliar mi conocimiento de las 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 las 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 puede explicarme eso, estaré más que agradecido.

Respuestas:


67

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

Puede usar la instanciación explícita para crear una instancia de una clase o función con plantilla sin usarla realmente en su código. Debido a que esto es útil cuando está creando archivos de biblioteca (.lib) que usan plantillas para la distribución, las definiciones de plantillas no autenticadas no se colocan en archivos de objeto (.obj).

(Por ejemplo, libstdc ++ contiene la instanciación explícita de std::basic_string<char,char_traits<char>,allocator<char> >(que es std::string), por lo que cada vez que usa funciones de std::string, no es necesario copiar el mismo código de función en los objetos. El compilador solo necesita hacer referencia (vincular) esos a libstdc ++.)


8
Sí, las bibliotecas MSVC CRT tienen instancias explícitas para todas las clases de secuencia, configuración regional y cadena, especializadas para char y wchar_t. El .lib resultante tiene más de 5 megabytes.
Hans Passant

4
¿Cómo sabe el compilador que la plantilla se ha instanciado explícitamente en otro lugar? ¿No va a generar simplemente la definición de clase porque está disponible?

@STing: si se crea una instancia de la plantilla, habrá una entrada de esas funciones en la tabla de símbolos.
kennytm

@Kenny: ¿Quieres decir si ya está instanciado en la misma TU? Asumiría que cualquier compilador es lo suficientemente inteligente como para no instanciar la misma especialización más de una vez en la misma TU. Pensé que el beneficio de la instanciación explícita (con respecto a los tiempos de compilación / enlace) es que si una especialización se instancia (explícitamente) en una TU, no se instanciará en las otras TU en las que se use. ¿No?

4
@Kenny: Conozco la opción GCC para evitar la instanciación implícita, pero esto no es un estándar. Hasta donde yo sé, VC ++ no tiene esa opción. Inst explícito. siempre se promociona como una mejora de los tiempos de compilación / enlace (incluso por Bjarne), pero para que sirva para ese propósito, el compilador debe saber de alguna manera que no debe instanciar implícitamente plantillas (por ejemplo, a través de la bandera GCC), o no debe recibir definición de plantilla, solo una declaración. ¿Suena esto correcto? Solo estoy tratando de entender por qué uno usaría la instanciación explícita (aparte de limitar los tipos concretos).

85

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

Coloque la declaración de la 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 una instancia explícita solo 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();
}

1
¿Es correcto decir que si el compilador tiene toda la definición de plantilla (incluyendo las definiciones de funciones) en una unidad de traducción dada, que será una instancia de una especialización de la plantilla cuando sea necesario (independientemente de que la especialización ha sido explícitamente una instancia en otro TU)? Es decir, para obtener los beneficios de la creación de instancias explícitas en tiempo de compilación / enlace, ¿solo se debe incluir la declaración de plantilla para que el compilador no pueda instanciarla?

1
@ user123456: Probablemente depende del compilador. Pero lo más probable es que sea cierto en la mayoría de las situaciones.
Martin York

1
¿Hay alguna manera de hacer que el compilador use esta versión explícitamente instanciada para los tipos que pre-especificas, pero luego, si intentas instanciar la plantilla con un tipo "extraño / inesperado", haz que funcione "como de costumbre", donde simplemente crea instancias de la plantilla según sea necesario?
David Doria

2
¿Cuál sería una buena verificación / prueba para asegurarse de que las instancias explícitas se estén utilizando realmente? Es decir, está funcionando, pero no estoy completamente convencido de que no se trate simplemente de instanciar todas las plantillas a pedido.
David Doria

7
La mayor parte de la charla de comentarios anterior ya no es cierta desde c ++ 11: una declaración de instanciación explícita (una plantilla externa) evita las instanciaciones implícitas: el código que de otro modo causaría una instanciación implícita tiene que usar la definición de instanciación explícita proporcionada en otro lugar programa (normalmente, en otro archivo: esto se puede usar para reducir los tiempos de compilación) en.cppreference.com/w/cpp/language/class_template
xaxxon

21

La creación de instancias explícita permite reducir los tiempos de compilación y el tamaño de los objetos

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 las herramientas de compilación reconstruyan los incluidos
  • redefinición de objetos

Eliminar definiciones de encabezados

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

Cuando la definición está en el encabezado y la modificas, un sistema de compilación inteligente recompilaría todos los includers, que podrían ser docenas de archivos, haciendo que la compilación sea 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 plantillas a una API externa" a continuación muestra una solución.

Vea ejemplos concretos a continuación.

Ganancias de 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 de argumento de plantilla diferente realizado.

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

Aquí está un ejemplo concreto, en el que tanto main.cppy notmain.cppdefinen implícitamente MyTemplate<int>debido a su uso en esos archivos.

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 aguas arriba .

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

Salida:

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 débil, que GCC eligió porque se trata de una función de plantilla. Símbolo débil significa que el código generado implícitamente para MyTemplate<int>se compiló en ambos archivos.

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 solo elige una de ellas para ponerla en el ejecutable final.

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

Podemos ver esto un poco más claramente 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 deshacerse.

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

Soluciones al problema de redefinición de objetos

Este problema puede evitarse mediante la creación de instancias explícita y:

  • mantenga la definición en hpp y agregue extern templatehpp para los tipos que se van a instanciar explícitamente.

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

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

    Abajo:

    • si es una biblioteca de solo encabezado, obliga a los proyectos externos a hacer su propia instanciación explícita. Si no es 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 un tipo integrado int, parece que se ve obligado a agregar la inclusión para él en el encabezado, una declaración hacia adelante no es suficiente: plantilla externa y tipos incompletos Esto aumenta las dependencias del encabezado un poco.
  • moviendo la definición en el archivo cpp, deje solo la declaración en hpp, es decir, modifique el ejemplo original para que sea:

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

    Desventaja: los proyectos externos no pueden usar su plantilla con sus propios tipos. También está obligado a crear una instancia explícita de todos los tipos. Pero tal vez esto sea una ventaja, ya que los programadores no lo olvidarán.

  • mantenga la definición en hpp y agregue extern templatecada 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); }
    

    Desventaja: todos los includers tienen que agregarlos externa sus archivos CPP, lo que los programadores probablemente olvidarán hacer.

Con cualquiera de esas soluciones, nmahora contiene:

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

Así que vemos tiene solamente mytemplate.otiene una compilación de MyTemplate<int>la forma deseada, mientras que notmain.oy main.ono lo hacen porque Ulos medios no definidos.

Elimine las definiciones de los encabezados incluidos, pero también exponga las plantillas de una API externa en una biblioteca de solo encabezados

Si su biblioteca no es solo de encabezado, el extern templatemétodo funcionará, ya que el uso de proyectos solo vinculará a su archivo de objeto, que contendrá el objeto de la instanciación de plantilla explícita.

Sin embargo, para bibliotecas solo de encabezado, si desea ambos:

  • acelera la compilación de tu proyecto
  • exponer los encabezados como una API de biblioteca externa para que otros la usen

entonces puedes probar una de las siguientes opciones:

    • mytemplate.hpp: definición de plantilla
    • mytemplate_interface.hpp: declaración de plantilla que solo coincide con las definiciones de mytemplate_interface.hpp, sin definiciones
    • mytemplate.cpp: incluir mytemplate.hppy crear instancias explícitas
    • main.cppy en cualquier otro lugar del código base: incluir mytemplate_interface.hpp, nomytemplate.hpp
    • mytemplate.hpp: definición de plantilla
    • mytemplate_implementation.hpp: incluye mytemplate.hppy agrega externa cada clase que se instanciará
    • mytemplate.cpp: incluir mytemplate.hppy crear instancias explícitas
    • main.cppy en cualquier otro lugar del código base: incluir mytemplate_implementation.hpp, nomytemplate.hpp

O incluso mejor quizás para varios encabezados: cree una carpeta intf/ impldentro de su includes/carpeta y utilícela mytemplate.hppcomo nombre siempre.

El mytemplate_interface.hppenfoque se ve así:

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

Compila y ejecuta:

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

Salida:

2

Probado en Ubuntu 18.04.

Módulos C ++ 20

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

Creo que esta función proporcionará la mejor configuración en el futuro a medida que esté disponible, pero aún no la he verificado porque aún no está disponible en mi GCC 9.2.1.

Aún tendrá que hacer una instanciación explícita para obtener la aceleración / guardar el disco, pero al menos tendremos una solución sensata para "Eliminar definiciones de los encabezados incluidos pero también exponer plantillas y una API externa" que no requiere copiar cosas alrededor de 100 veces.

El uso esperado (sin la iniciación explícita, no estoy seguro de cómo será la sintaxis exacta, consulte: ¿Cómo usar la instanciación explícita de la plantilla con módulos C ++ 20? )

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

y luego la compilación mencionada en 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

Entonces, a partir de esto, vemos que clang puede extraer la interfaz de la plantilla + implementación en la magia helloworld.pcm, que debe contener alguna representación intermedia LLVM de la fuente: ¿Cómo se manejan las plantillas en el sistema de módulos C ++? lo que aún permite que suceda la especificación de la plantilla.

Cómo analizar rápidamente su compilación para ver si ganaría mucho con la creación de instancias de plantillas

Entonces, ¿tiene un proyecto complejo y desea decidir si la creación de instancias de plantilla traerá ganancias significativas sin hacer la refactorización completa?

El análisis a continuación puede ayudarlo a decidir, o al menos seleccionar los objetos más prometedores para refactorizar primero mientras experimenta, tomando prestadas algunas ideas de: Mi archivo de objeto C ++ es demasiado 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

El sueño: una caché de compilador de plantillas

Creo que la solución definitiva sería si pudiéramos construir con:

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

y luego myfile.oreutilizaría automáticamente las plantillas compiladas previamente en todos los archivos.

Esto significaría 0 esfuerzo adicional para los programadores además de pasar esa opción CLI adicional a su sistema de compilación.

Una ventaja secundaria de la creación de instancias de plantilla explícita: los IDE de ayuda enumeran las instancias de plantilla

Descubrí que algunos IDE como Eclipse no pueden resolver "una lista de todas las instancias de plantilla utilizadas".

Por ejemplo, si está dentro de un código con plantilla y desea encontrar posibles valores de la plantilla, tendrá que encontrar los usos del constructor uno por uno y deducir los posibles tipos uno por uno.

Pero en Eclipse 2020-03 puedo enumerar fácilmente plantillas instanciadas explícitamente haciendo una búsqueda de Buscar todos los usos (Ctrl + Alt + G) en el nombre de la clase, que me señala, por ejemplo, desde:

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

a:

template class AnimalTemplate<Dog>;

Aquí hay una demostración: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Sin embargo, otra técnica de guerrila que podría usar fuera del IDE sería ejecutar nm -Cen el ejecutable final y hacer grep con el nombre de la plantilla:

nm -C main.out | grep AnimalTemplate

que apunta directamente al hecho de que Dogfue una de las instancias:

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.