c++ - cabecera - ¿Por qué solo se pueden implementar plantillas en el archivo de encabezado?
archivos de cabecera en c++ (14)
Aunque C ++ estándar no tiene tal requisito, algunos compiladores requieren que todas las plantillas de funciones y clases deban estar disponibles en cada unidad de traducción que se usen. En efecto, para esos compiladores, los cuerpos de las funciones de plantilla deben estar disponibles en un archivo de encabezado. Para repetir: eso significa que esos compiladores no permitirán que se definan en archivos que no son de cabecera, como los archivos .cpp
Hay una palabra clave de exportación que se supone que mitiga este problema, pero no es nada portátil.
Cita de la biblioteca estándar de C ++: un tutorial y un manual :
La única forma portátil de usar plantillas en este momento es implementarlas en archivos de encabezado mediante el uso de funciones en línea.
¿Por qué es esto?
(Aclaración: los archivos de encabezado no son la única solución portátil. Pero son la solución portátil más conveniente).
Aunque hay muchas explicaciones buenas arriba, me falta una forma práctica de separar las plantillas en encabezado y cuerpo.
Mi principal preocupación es evitar la recompilación de todos los usuarios de plantillas, cuando cambio su definición.
Tener todas las instancias de plantilla en el cuerpo de la plantilla no es una solución viable para mí, ya que el autor de la plantilla puede no saber todo si su uso y el usuario de la plantilla pueden no tener el derecho de modificarla.
Tomé el siguiente enfoque, que también funciona para compiladores más antiguos (gcc 4.3.4, aCC A.03.13).
Para cada uso de plantilla hay un typedef en su propio archivo de encabezado (generado a partir del modelo UML). Su cuerpo contiene la instanciación (que termina en una biblioteca que está vinculada al final).
Cada usuario de la plantilla incluye ese archivo de encabezado y usa el typedef.
Un ejemplo esquemático:
MyTemplate.h:
#ifndef MyTemplate_h
#define MyTemplate_h 1
template <class T>
class MyTemplate
{
public:
MyTemplate(const T& rt);
void dump();
T t;
};
#endif
MyTemplate.cpp:
#include "MyTemplate.h"
#include <iostream>
template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}
template <class T>
void MyTemplate<T>::dump()
{
cerr << t << endl;
}
MyInstantiatedTemplate.h:
#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"
typedef MyTemplate< int > MyInstantiatedTemplate;
#endif
MyInstantiatedTemplate.cpp:
#include "MyTemplate.cpp"
template class MyTemplate< int >;
main.cpp:
#include "MyInstantiatedTemplate.h"
int main()
{
MyInstantiatedTemplate m(100);
m.dump();
return 0;
}
De esta manera, solo será necesario volver a compilar las instancias de la plantilla, no todos los usuarios (y dependencias) de la plantilla.
El compilador generará código para cada instanciación de plantilla cuando use una plantilla durante el paso de compilación. En el proceso de compilación y enlace, los archivos .cpp se convierten en código de máquina o objeto puro que contiene referencias o símbolos indefinidos porque los archivos .h que se incluyen en main.cpp no tienen ninguna implementación. Estos están listos para vincularse con otro archivo de objeto que define una implementación para su plantilla y, por lo tanto, tiene un ejecutable a.out completo. Sin embargo, dado que las plantillas deben procesarse en el paso de compilación para generar el código para cada creación de instancias de plantilla que realice en su programa principal, la vinculación no ayudará porque compile main.cpp en main.o y luego compile su plantilla .cpp en template.o y luego el enlace no logrará el propósito de las plantillas porque estoy vinculando diferentes instancias de plantillas a la misma implementación de plantillas. Y se supone que las plantillas hacen lo contrario, es decir, tienen una implementación, pero permiten muchas instancias disponibles mediante el uso de una clase.
Lo que significa que el typename T
se reemplaza durante el paso de compilación, no el paso de vinculación, así que si intento compilar una plantilla sin que T
sea reemplazado como un tipo de valor concreto, entonces no funcionará porque esa es la definición de plantillas es un proceso de compilación, y por cierto la meta-programación tiene que ver con el uso de esta definición.
En realidad, antes de C ++ 11, el estándar definía la palabra clave de export
que permitiría declarar plantillas en un archivo de encabezado e implementarlas en otro lugar.
Ninguno de los compiladores populares implementó esta palabra clave. El único que conozco es la interfaz escrita por Edison Design Group, que es utilizada por el compilador Comeau C ++. Todos los demás requerían que escribiera plantillas en los archivos de encabezado, porque el compilador necesita la definición de la plantilla para una instanciación adecuada (como ya han señalado otros).
Como resultado, el comité del estándar ISO C ++ decidió eliminar la función de export
de plantillas con C ++ 11.
Es debido al requisito de compilación separada y porque las plantillas son polimorfismos de estilo de instanciación.
Vamos a acercarnos un poco más al concreto para una explicación. Digamos que tengo los siguientes archivos:
- foo.h
- declara la interfaz de la
class MyClass<T>
- declara la interfaz de la
- foo.cpp
- Define la implementación de la
class MyClass<T>
- Define la implementación de la
- bar.cpp
- utiliza
MyClass<int>
- utiliza
La compilación separada significa que debería poder compilar foo.cpp independientemente de bar.cpp . El compilador hace todo el trabajo duro de análisis, optimización y generación de código en cada unidad de compilación de forma completamente independiente; No necesitamos hacer un análisis completo del programa. Solo el enlazador necesita manejar todo el programa a la vez, y el trabajo del enlazador es mucho más fácil.
bar.cpp ni siquiera necesita existir cuando compilo foo.cpp , pero aún así debería poder vincular el foo.o que ya tenía junto con el bar.o que acabo de producir, sin necesidad de recompilar foo .cpp . foo.cpp incluso podría compilarse en una biblioteca dinámica, distribuirse en otro lugar sin foo.cpp y vincularse con el código que escriben años después de que escribiera foo.cpp .
"Polimorfismo de estilo de instanciación" significa que la plantilla MyClass<T>
no es realmente una clase genérica que se pueda compilar en un código que pueda funcionar para cualquier valor de T
Eso agregaría una sobrecarga como el boxeo, la necesidad de pasar punteros de función a asignadores y constructores, etc. La intención de las plantillas de C ++ es evitar tener que escribir la class MyClass_int
casi idéntica class MyClass_int
, la class MyClass_float
, etc., pero aún así ser capaz de terminar con Código compilado que es casi como si hubiéramos escrito cada versión por separado. Así que una plantilla es literalmente una plantilla; una plantilla de clase no es una clase, es una receta para crear una nueva clase para cada T
que encontramos. Una plantilla no se puede compilar en un código, solo se puede compilar el resultado de crear una instancia de la plantilla.
Entonces, cuando se compila foo.cpp , el compilador no puede ver bar.cpp para saber que se necesita MyClass<int>
. Puede ver la plantilla MyClass<T>
, pero no puede emitir código para eso (es una plantilla, no una clase). Y cuando compila bar.cpp , el compilador puede ver que necesita crear una MyClass<int>
, pero no puede ver la plantilla MyClass<T>
(solo su interfaz en foo.h ) por lo que no puede crear eso.
Si foo.cpp utiliza MyClass<int>
, el código para eso se generará al compilar foo.cpp , de modo que cuando bar.o esté vinculado a foo.o , podrán conectarse y funcionarán. Podemos usar ese hecho para permitir que se implemente un conjunto finito de instancias de plantillas en un archivo .cpp escribiendo una sola plantilla. Pero no hay forma de que bar.cpp use la plantilla como plantilla y cree instancias de los tipos que quiera; solo puede usar versiones preexistentes de la clase de plantilla que el autor de foo.cpp pensó proporcionar.
Podría pensar que al compilar una plantilla el compilador debería "generar todas las versiones", y las que nunca se usan se filtran durante el enlace. Aparte de la enorme sobrecarga y las dificultades extremas a las que se enfrentaría un enfoque de este tipo porque las características de "modificador de tipo", como los punteros y las matrices, permiten que incluso los tipos incorporados den lugar a un número infinito de tipos, ¿qué sucede cuando ahora extiendo mi programa? añadiendo:
- baz.cpp
- declara e implementa la
class BazPrivate
y utilizaMyClass<BazPrivate>
- declara e implementa la
No hay forma posible de que esto funcione a menos que nosotros
- Debo recompilar foo.cpp cada vez que cambiemos cualquier otro archivo en el programa , en caso de que agregue una nueva instancia de
MyClass<T>
- Requerir que baz.cpp contenga (posiblemente a través del encabezado incluye) la plantilla completa de
MyClass<T>
, para que el compilador pueda generarMyClass<BazPrivate>
durante la compilación de baz.cpp .
A nadie le gusta (1), porque los sistemas de compilación de análisis de todo el programa tardan una eternidad en compilarse, y porque hace que sea imposible distribuir bibliotecas compiladas sin el código fuente. Así que tenemos (2) en su lugar.
Eso es exactamente correcto porque el compilador tiene que saber qué tipo es para la asignación. Por lo tanto, las clases de plantilla, funciones, enumeraciones, etc. deben implementarse también en el archivo de encabezado si se va a hacer pública o parte de una biblioteca (estática o dinámica) porque los archivos de encabezado NO se compilan a diferencia de los archivos c / cpp que son. Si el compilador no sabe el tipo no puede compilarlo. En .Net puede porque todos los objetos derivan de la clase Object. Esto no es .Net.
Esto significa que la forma más portátil de definir implementaciones de métodos de clases de plantilla es definirlas dentro de la definición de clase de plantilla.
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
Las plantillas deben ser instanciadas por el compilador antes de compilarlas en el código objeto. Esta creación de instancias solo se puede lograr si se conocen los argumentos de la plantilla. Ahora imagine un escenario donde una función de plantilla se declara en ah
, se define en a.cpp
y se usa en b.cpp
. Cuando se compila a.cpp
, no se sabe necesariamente que la próxima compilación b.cpp
requerirá una instancia de la plantilla, y mucho menos qué instancia específica sería esa. Para más archivos de encabezado y origen, la situación puede complicarse rápidamente.
Se puede argumentar que los compiladores pueden ser más inteligentes para "mirar hacia adelante" para todos los usos de la plantilla, pero estoy seguro de que no sería difícil crear escenarios recursivos o complicados. AFAIK, los compiladores no miran hacia delante. Como señaló Anton, algunos compiladores admiten declaraciones de exportación explícitas de instancias de plantillas, pero no todos los compiladores lo admiten (¿todavía?).
Las plantillas deben usarse en los encabezados porque el compilador necesita crear instancias de diferentes versiones del código, dependiendo de los parámetros dados / deducidos para los parámetros de la plantilla. Recuerde que una plantilla no representa el código directamente, sino una plantilla para varias versiones de ese código. Cuando compila una función que no es de plantilla en un archivo .cpp
, está compilando una función / clase concreta. Este no es el caso de las plantillas, que pueden ser instanciadas con diferentes tipos, es decir, el código concreto debe emitirse al reemplazar los parámetros de la plantilla con tipos concretos.
Había una característica con la palabra clave de export
que estaba destinada a ser utilizada para la compilación por separado. La función de export
está en desuso en C++11
y, AFAIK, solo un compilador la implementó. No debes hacer uso de la export
. La compilación separada no es posible en C++
o C++11
pero tal vez en C++17
, si los conceptos lo logran, podríamos tener alguna forma de compilación separada.
Para lograr una compilación por separado, debe ser posible verificar el cuerpo de la plantilla por separado. Parece que una solución es posible con conceptos. Eche un vistazo a este paper presentado recientemente en la reunión del comité de normas. Creo que este no es el único requisito, ya que todavía necesita crear una instancia del código de la plantilla en el código de usuario.
El problema de compilación por separado para las plantillas Supongo que también es un problema que surge con la migración a los módulos, que actualmente se está trabajando.
Muchas respuestas correctas aquí, pero quería agregar esto (para completar):
Si, en la parte inferior del archivo cpp de implementación, realiza una creación de instancias explícita de todos los tipos con los que se utilizará la plantilla, el enlazador podrá encontrarlos como de costumbre.
Edición: Agregar ejemplo de creación de instancias explícita de plantillas. Se utiliza después de que se haya definido la plantilla y se hayan definido todas las funciones miembro.
template class vector<int>;
Esto instanciará (y, por lo tanto, pondrá a disposición del vinculador) la clase y todas sus funciones miembro (solo). La sintaxis similar funciona con las funciones de la plantilla, por lo que si tiene sobrecargas de operadores que no son miembros, es posible que deba hacer lo mismo con esas.
El ejemplo anterior es bastante inútil ya que el vector está completamente definido en los encabezados, excepto cuando un archivo de inclusión común (¿encabezado precompilado?) Usa el extern template class vector<int>
para evitar que se cree una instancia en todos los otros archivos (1000?) que usan el vector.
No es necesario colocar la implementación en el archivo de encabezado, vea la solución alternativa al final de esta respuesta.
De todos modos, la razón por la que su código está fallando es que, al crear una instancia de una plantilla, el compilador crea una nueva clase con el argumento de la plantilla dada. Por ejemplo:
template<typename T>
struct Foo
{
T bar;
void doSomething(T param) {/* do stuff using T */}
};
// somewhere in a .cpp
Foo<int> f;
Al leer esta línea, el compilador creará una nueva clase (llamémosla FooInt
), que es equivalente a lo siguiente:
struct FooInt
{
int bar;
void doSomething(int param) {/* do stuff using int */}
}
En consecuencia, el compilador necesita tener acceso a la implementación de los métodos, para instanciarlos con el argumento de la plantilla (en este caso int
). Si estas implementaciones no estuvieran en el encabezado, no serían accesibles y, por lo tanto, el compilador no podría crear una instancia de la plantilla.
Una solución común a esto es escribir la declaración de la plantilla en un archivo de encabezado, luego implementar la clase en un archivo de implementación (por ejemplo, .tpp) e incluir este archivo de implementación al final del encabezado.
// Foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
De esta manera, la implementación aún está separada de la declaración, pero es accesible para el compilador.
Otra solución es mantener la implementación separada e instanciar explícitamente todas las instancias de plantilla que necesitará:
// Foo.h
// no implementation
template <typename T> struct Foo { ... };
//----------------------------------------
// Foo.cpp
// implementation of Foo''s methods
// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float
Si mi explicación no es lo suficientemente clara, puede consultar las Súper preguntas frecuentes de C ++ sobre este tema .
Si el problema es el tiempo de compilación adicional y el tamaño binario generado al compilar el .h como parte de todos los módulos .cpp que lo utilizan, en muchos casos, lo que puede hacer es hacer que la clase de plantilla descienda de una clase base no templada. partes no dependientes de tipo de la interfaz, y esa clase base puede tener su implementación en el archivo .cpp.
Solo para agregar algo digno de mención aquí. Uno puede definir los métodos de una clase con plantilla muy bien en el archivo de implementación cuando no son plantillas de funciones.
myQueue.hpp:
template <class T>
class QueueA {
int size;
...
public:
template <class T> T dequeue() {
// implementation here
}
bool isEmpty();
...
}
myQueue.cpp:
// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
return this->size == 0;
}
main()
{
QueueA<char> Q;
...
}
Una forma de tener una implementación separada es la siguiente.
//inner_foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
//foo.h
#include <foo.tpp>
//main.cpp
#include <foo.h>
inner_foo tiene las declaraciones a plazo. foo.tpp tiene la implementación e incluye inner_foo.h; y foo.h tendrá solo una línea, para incluir foo.tpp.
En el tiempo de compilación, los contenidos de foo.h se copian a foo.tpp y luego se copia el archivo completo a foo.h, luego de lo cual se compila. De esta manera, no hay limitaciones, y la asignación de nombres es consistente, a cambio de un archivo adicional.
Hago esto porque los analizadores estáticos para el código se rompen cuando no ve las declaraciones directas de la clase en * .tpp. Esto es molesto al escribir código en cualquier IDE o al usar YouCompleteMe u otros.