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.
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(); }
fuente
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 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.cpp
ynotmain.cpp
definen implícitamenteMyTemplate<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 queW
significa 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 paraMyTemplate<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ón0000000000000017
: tamaño del código generado para ellosPodemos ver esto un poco más claramente con:
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
_ZN10MyTemplateIiE1fEi
es el nombre destrozado delMyTemplate<int>::f(int)>
quec++filt
decidió 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 template
hpp 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:
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 template
cada 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
extern
a sus archivos CPP, lo que los programadores probablemente olvidarán hacer.Con cualquiera de esas soluciones,
nm
ahora 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.o
tiene una compilación deMyTemplate<int>
la forma deseada, mientras quenotmain.o
ymain.o
no lo hacen porqueU
los 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 template
mé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:
entonces puedes probar una de las siguientes opciones:
mytemplate.hpp
: definición de plantillamytemplate_interface.hpp
: declaración de plantilla que solo coincide con las definiciones demytemplate_interface.hpp
, sin definicionesmytemplate.cpp
: incluirmytemplate.hpp
y crear instancias explícitasmain.cpp
y en cualquier otro lugar del código base: incluirmytemplate_interface.hpp
, nomytemplate.hpp
mytemplate.hpp
: definición de plantillamytemplate_implementation.hpp
: incluyemytemplate.hpp
y agregaextern
a cada clase que se instanciarámytemplate.cpp
: incluirmytemplate.hpp
y crear instancias explícitasmain.cpp
y en cualquier otro lugar del código base: incluirmytemplate_implementation.hpp
, nomytemplate.hpp
O incluso mejor quizás para varios encabezados: cree una carpeta
intf
/impl
dentro de suincludes/
carpeta y utilícelamytemplate.hpp
como nombre siempre.El
mytemplate_interface.hpp
enfoque 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.o
reutilizarí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 -C
en el ejecutable final y hacer grep con el nombre de la plantilla:que apunta directamente al hecho de que
Dog
fue una de las instancias:0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]() 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog) 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
fuente
Depende del modelo del compilador; aparentemente existe el modelo Borland y el modelo CFront. Y luego depende también de su intención: si está escribiendo una biblioteca, podría (como se mencionó anteriormente) crear una instancia explícita de las especializaciones que desee.
La página de GNU c ++ analiza los modelos aquí https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .
fuente