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 utilizando 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).
Respuestas:
Advertencia: no es necesario poner la implementación en el archivo de encabezado; consulte la solución alternativa al final de esta respuesta.
De todos modos, la razón por la que su código falla es que, al instanciar una plantilla, el compilador crea una nueva clase con el argumento de plantilla dado. Por ejemplo:
Al leer esta línea, el compilador creará una nueva clase (llamémosla
FooInt
), que es equivalente a lo siguiente: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 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
Foo.tpp
De esta forma, la implementación aún está separada de la declaración, pero el compilador puede acceder a ella.
Solución alternativa
Otra solución es mantener la implementación separada y crear una instancia explícita de todas las instancias de plantilla que necesitará:
Foo.h
Foo.cpp
Si mi explicación no es lo suficientemente clara, puede echar un vistazo a las preguntas frecuentes de C ++ Super sobre este tema .
fuente
Muchas respuestas correctas aquí, pero quería agregar esto (para completar):
Si usted, en la parte inferior del archivo cpp de implementación, hace una instancia explícita de todos los tipos con los que se usará la plantilla, el vinculador podrá encontrarlos como de costumbre.
Editar: Agregar un ejemplo de creación de instancias explícita de plantilla. Se usa después de que se ha definido la plantilla y se han definido todas las funciones miembro.
Esto creará una instancia (y, por lo tanto, pondrá a disposición del vinculador) la clase y todas sus funciones miembro (solo). Una sintaxis similar funciona para las funciones de plantilla, por lo que si tiene sobrecargas de operadores que no son miembros, es posible que deba hacer lo mismo para ellas.
El ejemplo anterior es bastante inútil ya que el vector está completamente definido en los encabezados, excepto cuando se usa un archivo de inclusión común (¿encabezado precompilado?)
extern template class vector<int>
Para evitar que se instancia en todos los otros archivos (1000?) Que usan el vector.fuente
type
sin enumerarlos manualmente.vector
no es un buen ejemplo porque un contenedor está inherentemente dirigido a "todos" los tipos. Pero sucede con frecuencia que crea plantillas que solo están destinadas a un conjunto específico de tipos, por ejemplo, tipos numéricos: int8_t, int16_t, int32_t, uint8_t, uint16_t, etc. En este caso, todavía tiene sentido usar una plantilla , pero también es posible crear instancias explícitas para todo el conjunto de tipos y, en mi opinión, recomendado..cpp
archivo de la clase y las dos instancias se mencionan desde otros.cpp
archivos, y todavía recibo el error de vinculación de que no se encuentran los miembros.Es por el 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:
class MyClass<T>
class MyClass<T>
MyClass<int>
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. Es solo el enlazador el que necesita manejar todo el programa a la vez, y el trabajo del enlazador es sustancialmente 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 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 escribí foo.cpp .
"Polimorfismo de estilo de instanciación" significa que la plantilla
MyClass<T>
no es realmente una clase genérica que pueda compilarse en un código que pueda funcionar para cualquier valor deT
. Eso sería agregar una sobrecarga como el boxeo, necesidad de pasar punteros de función para asignadores y constructores, etc. La intención de plantillas de C ++ es evitar tener que escribir casi idénticaclass MyClass_int
,class MyClass_float
, etc, pero que todavía será capaz de terminar con el código compilado que es sobre todo como si nos habíamos escrito cada versión por separado. Entonces, una plantilla es literalmente una plantilla; una plantilla de clase no es una clase, es una receta para crear una nueva clase para cada unoT
que encontremos. Una plantilla no se puede compilar en código, solo se puede compilar el resultado de instanciar la plantilla.Entonces, cuando se compila foo.cpp , el compilador no puede ver bar.cpp para saber que
MyClass<int>
es necesario. Puede ver la plantillaMyClass<T>
, pero no puede emitir código para eso (es una plantilla, no una clase). Y cuando se compila bar.cpp , el compilador puede ver que necesita crear unaMyClass<int>
, pero no puede ver la plantillaMyClass<T>
(solo su interfaz en foo.h ), por lo que no puede crearla.Si usa foo.cpp
MyClass<int>
, entonces se generará el código al compilar foo.cpp , de modo que cuando bar.o esté vinculado a foo.o, se puedan conectar y funcionarán. Podemos usar ese hecho para permitir que se implemente un conjunto finito de instancias de plantilla en un archivo .cpp escribiendo una sola plantilla. Pero no hay forma de que bar.cpp use la plantilla como plantilla y la instancia en los tipos que le gusten; solo puede usar versiones preexistentes de la clase con plantilla que el autor de foo.cpp pensó proporcionar.Puede pensar que al compilar una plantilla, el compilador debe "generar todas las versiones", y las que nunca se utilizan se filtran durante el enlace. Además de los enormes gastos generales y las dificultades extremas que tal enfoque enfrentaría porque las características de "modificador de tipo" como punteros y matrices permiten que incluso los tipos incorporados den lugar a un número infinito de tipos, lo que sucede cuando ahora extiendo mi programa añadiendo:
class BazPrivate
, y usaMyClass<BazPrivate>
No hay forma posible de que esto funcione a menos que
MyClass<T>
MyClass<T>
, de modo 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. Entonces tenemos (2) en su lugar.
fuente
El compilador debe crear instancias de las plantillas antes de compilarlas en el código objeto. Esta instanciación solo se puede lograr si se conocen los argumentos de la plantilla. Ahora imagine un escenario en el que se declara
a.h
, definea.cpp
y utiliza una función de plantillab.cpp
. Cuandoa.cpp
se compila, no se sabe necesariamente que la próxima compilaciónb.cpp
requerirá una instancia de la plantilla, y mucho menos qué instancia específica sería esa. Para obtener más archivos de encabezado y fuente, la situación puede volverse más complicada 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 se ven tan mal. Como señaló Anton, algunos compiladores admiten declaraciones explícitas de exportación de instancias de plantillas, pero no todos los compiladores lo admiten (¿todavía?).
fuente
En realidad, antes de C ++ 11, el estándar definía la
export
palabra clave 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 requieren que escriba plantillas en los archivos de encabezado, porque el compilador necesita la definición de la plantilla para una instanciación adecuada (como ya señalaron otros).
Como resultado, el comité estándar ISO C ++ decidió eliminar la
export
característica de las plantillas con C ++ 11.fuente
export
que realmente nos habría dado , y lo que no ... y ahora estoy totalmente de acuerdo con la gente de EDG: No nos habría traído lo que la mayoría de la gente (yo mismo en el '11 incluido) creo que lo haría, y el estándar C ++ está mejor sin él.Aunque el estándar C ++ no tiene ese requisito, algunos compiladores requieren que todas las plantillas de función y clase estén 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 sean encabezados, como archivos .cpp
Hay una palabra clave de exportación que se supone que mitiga este problema, pero no está cerca de ser portátil.
fuente
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
.cpp
archivo, está compilando una función / clase concreta. Este no es el caso para las plantillas, que se pueden instanciar con diferentes tipos, a saber, se debe emitir un código concreto al reemplazar los parámetros de la plantilla con tipos concretos.Había una característica con la
export
palabra clave que debía usarse para una compilación separada. losexport
característica está en desusoC++11
y, AFAIK, solo un compilador la implementó. No deberías hacer uso deexport
. No es posible compilar por separado enC++
o,C++11
pero tal vez enC++17
, si los conceptos lo hacen, podríamos tener alguna forma de compilación por separado.Para lograr una compilación por separado, debe ser posible la verificación del cuerpo de la plantilla por separado. Parece que una solución es posible con conceptos. Eche un vistazo a este documento presentado recientemente en la reunión del comité de normas. Creo que este no es el único requisito, ya que aún necesita crear instancias de código para el código de plantilla en el código de usuario.
Supongo que el problema de compilación por separado para las plantillas también es un problema que surge con la migración a los módulos, que actualmente se está trabajando.
fuente
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.
fuente
Aunque hay muchas buenas explicaciones anteriores, 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 modificarlo.
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 typedef.
Un ejemplo esquemático:
MyTemplate.h:
MyTemplate.cpp:
MyInstantiatedTemplate.h:
MyInstantiatedTemplate.cpp:
main.cpp:
De esta forma, solo se deberán volver a compilar las instancias de la plantilla, no todos los usuarios de la plantilla (y sus dependencias).
fuente
MyInstantiatedTemplate.h
archivo y elMyInstantiatedTemplate
tipo agregado . Es un poco más limpio si no lo usas, en mi humilde opinión. Vea mi respuesta en una pregunta diferente que muestra esto: stackoverflow.com/a/41292751/4612476Solo para agregar algo digno de mención aquí. Uno puede definir métodos de una clase con plantilla muy bien en el archivo de implementación cuando no son plantillas de función.
myQueue.hpp:
myQueue.cpp:
fuente
isEmpty
desde ninguna otra unidad de traducción además demyQueue.cpp
...Si la preocupación es el tiempo de compilación adicional y la hinchazón de tamaño binario producida compilando el .h como parte de todos los módulos .cpp que lo usan, en muchos casos lo que puede hacer es hacer que la clase de plantilla descienda de una clase base no templada para partes no dependientes del tipo de la interfaz, y esa clase base puede tener su implementación en el archivo .cpp.
fuente
class XBase
donde sea que necesite implementar untemplate class X
, colocando las partes dependientes del tipoX
y todo el restoXBase
.Eso es exactamente correcto porque el compilador tiene que saber de qué tipo es para la asignación. Por lo tanto, las clases de plantilla, funciones, enumeraciones, etc. también deben implementarse en el archivo de encabezado si se va a hacer público 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.
fuente
El compilador generará código para cada instancia de plantilla cuando use una plantilla durante el paso de compilación. En el proceso de compilación y vinculación, los archivos .cpp se convierten a objetos puros o códigos de máquina que contienen referencias o símbolos indefinidos porque los archivos .h que se incluyen en su main.cpp aún no tienen 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 completo a.out.
Sin embargo, dado que las plantillas deben procesarse en el paso de compilación para generar código para cada instancia de plantilla que defina, por lo que simplemente compilar una plantilla separada de su archivo de encabezado no funcionará porque siempre van de la mano, por la misma razón que cada instancia de plantilla es una clase completamente nueva literalmente. En una clase regular, puede separar .h y .cpp porque .h es un modelo de esa clase y .cpp es la implementación en bruto, por lo que cualquier archivo de implementación se puede compilar y vincular regularmente, sin embargo, el uso de plantillas .h es un modelo de cómo la clase no debería verse como debería verse el objeto, lo que significa que un archivo de plantilla .cpp no es una implementación regular sin formato de una clase, es simplemente un plano para una clase, por lo que cualquier implementación de un archivo de plantilla .h puede '
Por lo tanto, las plantillas nunca se compilan por separado y solo se compilan donde sea que tenga una instanciación concreta en algún otro archivo fuente. Sin embargo, la instanciación concreta necesita conocer la implementación del archivo de plantilla, ya que simplemente modificando el
typename T
usar un tipo concreto en el archivo .h no va a hacer el trabajo porque lo que .cpp está allí para vincular, no puedo encontrarlo más adelante porque recordar plantillas son abstractas y no se pueden compilar, así que me veo obligado para dar la implementación en este momento, así sé qué compilar y vincular, y ahora que tengo la implementación, se vincula al archivo fuente adjunto. Básicamente, en el momento en que instancia una plantilla, necesito crear una clase completamente nueva, y no puedo hacer eso si no sé cómo debería ser esa clase cuando uso el tipo que proporciono a menos que notifique al compilador de la implementación de la plantilla, por lo que ahora el compilador puede reemplazarT
con mi tipo y crear una clase concreta que esté lista para ser compilada y vinculada.En resumen, las plantillas son planos de cómo deberían verse las clases, las clases son planos de cómo debería verse un objeto. No puedo compilar plantillas separadas de su instanciación concreta porque el compilador solo compila tipos concretos, en otras palabras, plantillas al menos en C ++, es pura abstracción del lenguaje. Tenemos que restar plantillas, por así decirlo, y lo hacemos dándoles un tipo concreto con el que tratar para que nuestra abstracción de plantillas pueda transformarse en un archivo de clase regular y, a su vez, pueda compilarse normalmente. Separar el archivo de plantilla .h y el archivo de plantilla .cpp no tiene sentido. No tiene sentido porque la separación de .cpp y .h solo es donde el .cpp puede compilarse individualmente y vincularse individualmente, con plantillas, ya que no podemos compilarlas por separado, porque las plantillas son una abstracción,
Lo que significa
typename T
que se reemplaza durante el paso de compilación, no el paso de vinculación, por lo que si trato de compilar una plantilla sinT
ser reemplazado como un tipo de valor concreto que no tiene ningún significado para el compilador y, como resultado, el código objeto no se puede crear porque no saber lo queT
esEs técnicamente posible crear algún tipo de funcionalidad que guardará el archivo template.cpp y cambiará los tipos cuando los encuentre en otras fuentes, creo que el estándar tiene una palabra clave
export
que le permitirá colocar plantillas en un archivo separado archivo cpp pero no muchos compiladores realmente implementan esto.Solo una nota al margen, al hacer especializaciones para una clase de plantilla, puede separar el encabezado de la implementación porque una especialización, por definición, significa que me estoy especializando en un tipo concreto que se puede compilar y vincular individualmente.
fuente
Una forma de tener una implementación separada es la siguiente.
inner_foo tiene las declaraciones hacia adelante. foo.tpp tiene la implementación e incluye inner_foo.h; y foo.h tendrá solo una línea, para incluir foo.tpp.
En tiempo de compilación, el contenido de foo.h se copia en foo.tpp y luego todo el archivo se copia en foo.h, luego de lo cual se compila. De esta manera, no hay limitaciones, y la denominación es coherente, 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 clase en * .tpp. Esto es molesto al escribir código en cualquier IDE o al usar YouCompleteMe u otros.
fuente
Sugiero mirar esta página de gcc que discute las compensaciones entre el modelo "cfront" y el "borland" para las instancias de plantillas.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
El modelo "borland" corresponde a lo que sugiere el autor, proporcionando la definición completa de la plantilla y compilando las cosas varias veces.
Contiene recomendaciones explícitas sobre el uso de la creación de instancias de plantilla manual y automática. Por ejemplo, la opción "-repo" se puede utilizar para recopilar plantillas que deben instanciarse. O bien, otra opción es deshabilitar las instancias automáticas de plantillas usando "-fno-implicit-templates" para forzar la creación manual de instancias de plantillas.
En mi experiencia, confío en que las instancias de la Biblioteca estándar de C ++ y Boost se instancian para cada unidad de compilación (usando una biblioteca de plantillas). Para mis clases de plantillas grandes, hago instanciación manual de plantillas, una vez, para los tipos que necesito.
Este es mi enfoque porque estoy proporcionando un programa de trabajo, no una biblioteca de plantillas para usar en otros programas. El autor del libro, Josuttis, trabaja mucho en bibliotecas de plantillas.
Si realmente me preocupara la velocidad, supongo que exploraría el uso de encabezados precompilados https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
que está ganando soporte en muchos compiladores. Sin embargo, creo que los encabezados precompilados serían difíciles con los archivos de encabezado de plantilla.
fuente
Otra razón por la que es una buena idea escribir declaraciones y definiciones en los archivos de encabezado es para facilitar la lectura. Supongamos que existe una función de plantilla en Utility.h:
Y en Utility.cpp:
Esto requiere que cada clase T aquí implemente el operador menor que (<). Lanzará un error de compilación cuando compare dos instancias de clase que no hayan implementado el "<".
Por lo tanto, si separa la declaración y la definición de la plantilla, no podrá leer solo el archivo de encabezado para ver los entresijos de esta plantilla para usar esta API en sus propias clases, aunque el compilador le dirá en este caso sobre qué operador debe ser anulado.
fuente
En realidad, puede definir su clase de plantilla dentro de un archivo .template en lugar de un archivo .cpp. Quien diga que solo puede definirlo dentro de un archivo de encabezado está equivocado. Esto es algo que funciona desde c ++ 98.
No olvide que su compilador trate su archivo .template como un archivo c ++ para mantener el sentido de inteligencia.
Aquí hay un ejemplo de esto para una clase de matriz dinámica.
Ahora dentro de su archivo .template define sus funciones exactamente como lo haría normalmente.
fuente