¿Por qué las plantillas solo se pueden implementar en el archivo de encabezado?

Jan 30 2009

Cita de la biblioteca estándar de C ++: tutorial y manual :

La única forma portátil de usar plantillas en este momento es implementarlas en archivos de encabezado usando 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

1635 LucTouraille Jan 30 2009 at 17:26

Advertencia: no es necesario colocar 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 está fallando es que, al crear una instancia de una plantilla, el compilador crea una nueva clase con el argumento de plantilla dado. 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 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.

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

// 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 echar un vistazo a C ++ Super-FAQ sobre este tema .

271 Ben May 11 2013 at 10:54

Se debe al requisito de una compilación separada y a que las plantillas son polimorfismos de estilo de instanciación.

Acerquémonos un poco más al concreto para obtener una explicación. Digamos que tengo los siguientes archivos:

  • foo.h
    • declara la interfaz de class MyClass<T>
  • foo.cpp
    • define la implementación de class MyClass<T>
  • bar.cpp
    • usos 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 de todo el 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 debería poder vincular el foo . o ya lo tenía junto con el bar . o Acabo de producir, sin necesidad de volver a compilar 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 escribieron 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 se pueda compilar en un código que pueda funcionar con cualquier valor de T. 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éntica class 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 una Tque 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 plantilla MyClass<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 un MyClass<int>, pero no puede ver la plantilla MyClass<T>(solo su interfaz en foo.h ) por lo que no puede crearlo.

Si foo.cpp se usa MyClass<int>, entonces se generará código para eso mientras se compila foo.cpp , por lo que cuando bar.o está vinculado a foo.o, se pueden conectar 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 la instancia en los tipos que desee; solo puede usar versiones preexistentes de la clase con 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 utilizan se filtran durante la vinculación. Aparte de la enorme sobrecarga y las dificultades extremas que enfrentaría un enfoque de este tipo porque las características del "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 class BazPrivate, y usaMyClass<BazPrivate>

No hay forma posible de que esto funcione a menos que

  1. Tenemos que recompilar foo.cpp cada vez que cambiemos cualquier otro archivo en el programa , en caso de que agregue una nueva instanciación novedosa deMyClass<T>
  2. Requiere que baz.cpp contenga (posiblemente a través de encabezados incluidos) la plantilla completa de MyClass<T>, para que el compilador pueda generar MyClass<BazPrivate>durante la compilación de baz.cpp .

A nadie le gusta (1), porque los sistemas de compilación de análisis de programas completos tardan una eternidad en compilarse y porque hace imposible distribuir bibliotecas compiladas sin el código fuente. Entonces tenemos (2) en su lugar.

252 MaHuJa Aug 13 2009 at 20:49

Muchas respuestas correctas aquí, pero quería agregar esto (para completar):

Si, en la parte inferior del archivo cpp de implementación, crea una instancia explícita de todos los tipos con los que se utilizará la plantilla, el vinculador podrá encontrarlos como de costumbre.

Editar: agregando un ejemplo de instanciación de plantilla explícita. Se utiliza después de que se haya definido la plantilla y se hayan definido todas las funciones de los miembros.

template class vector<int>;

Esto instanciará (y por lo tanto pondrá a disposición del enlazador) 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 con 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 cree una instancia en todos los demás archivos (1000?) Que usan vector.

86 DavidHanak Jan 30 2009 at 17:23

El compilador debe crear una instancia 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, define a.cppy utiliza una función de plantilla b.cpp. Cuando a.cppse compila, no se sabe necesariamente que la próxima compilación b.cpprequerirá una instancia de la plantilla, y mucho menos qué instancia específica sería esa. Para más archivos fuente y de encabezado, la situación puede complicarse rápidamente.

Se puede argumentar que los compiladores pueden hacerse 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 hacen esas miradas por adelantado. 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?).

65 DevSolar Jan 30 2009 at 20:38

En realidad, antes de C ++ 11, el estándar definía la exportpalabra clave que haría posible 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 el frontend escrito por Edison Design Group, que es utilizado por el compilador Comeau C ++. Todos los demás requerían que escribiera plantillas en archivos de encabezado, porque el compilador necesita la definición de la plantilla para la creación de instancias adecuada (como ya señalaron otros).

Como resultado, el comité de estándares ISO C ++ decidió eliminar la exportfunción de las plantillas con C ++ 11.

34 AntonGogolev Jan 30 2009 at 17:15

Aunque el C ++ estándar no tiene tal requisito, algunos compiladores requieren que todas las plantillas de funciones y clases estén disponibles en cada unidad de traducción que se utilicen. 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 de encabezado, como archivos .cpp

Hay una palabra clave de exportación que se supone que mitiga este problema, pero no es ni mucho menos portátil.

29 GermánDiago May 12 2013 at 23:42

Las plantillas deben usarse en los encabezados porque el compilador necesita instanciar 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 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 .cpparchivo, está compilando una función / clase concreta. Este no es el caso de las plantillas, que se pueden instanciar con diferentes tipos, es decir, se debe emitir código concreto cuando se reemplazan los parámetros de la plantilla con tipos concretos.

Había una función con la exportpalabra clave que estaba destinada a utilizarse para una compilación independiente. La exportfunción está obsoleta C++11y, AFAIK, solo un compilador la implementó. No deberías hacer uso de export. La compilación separada no es posible en C++o, C++11pero tal vez en C++17, si los conceptos entran, podríamos tener alguna forma de compilación separada.

Para lograr una compilación separada, 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 una instancia del código para el código de plantilla en el código de usuario.

Supongo que el problema de la compilación por separado para las plantillas también es un problema que está surgiendo con la migración a los módulos, que actualmente se está trabajando.

EDITAR: A partir de agosto de 2020, los módulos ya son una realidad para C ++: https://en.cppreference.com/w/cpp/language/modules

18 lafrecciablu May 12 2016 at 21:02

Aunque hay muchas buenas explicaciones arriba, me falta una forma práctica de separar las plantillas en encabezado y cuerpo.
Mi principal preocupación es evitar la recopilación de todos los usuarios de la plantilla, cuando cambio su definición.
Tener todas las instancias de la plantilla en el cuerpo de la plantilla no es una solución viable para mí, ya que es posible que el autor de la plantilla no sepa 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;
}

MyInstanticatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstanticatedTemplate.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 de la plantilla (y dependencias).

16 Benoît Jan 30 2009 at 17:53

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
    }
};
9 Nikos Jul 19 2018 at 07:49

Solo para agregar algo digno de mención aquí. Se pueden definir bien los métodos de una clase con plantilla 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;

    ...
}
7 EricShaw Jul 27 2016 at 12:01

Si la preocupación es el tiempo de compilación adicional y la hinchazón del tamaño binario que se produce al compilar 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 sin plantilla para partes no dependientes del tipo de la interfaz, y esa clase base puede tener su implementación en el archivo .cpp.

6 Robert Sep 17 2011 at 10:40

Eso es exactamente correcto porque el compilador debe saber 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 conoce el tipo, no puede compilarlo. En .Net puede porque todos los objetos se derivan de la clase Object. Esto no es .Net.

6 MosheRabaev Jul 19 2016 at 08:10

El compilador generará código para cada instanciación de plantilla cuando utilice una plantilla durante el paso de compilación. En el proceso de compilación y vinculación, los archivos .cpp se convierten a un objeto puro o código de máquina que en ellos contiene 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 ser vinculados 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 código para cada instanciación de plantilla que defina, 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 instanciación de plantilla es una clase completamente nueva literalmente. En una clase normal, puede separar .hy .cpp porque .h es un modelo de esa clase y .cpp es la implementación sin formato, 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 cómo debería verse el objeto, lo que significa que un archivo .cpp de plantilla no es una implementación normal sin procesar de una clase, es simplemente un modelo para una clase, por lo que cualquier implementación de un archivo de plantilla .h no se puede compilar porque necesita algo concreto para compilar, las plantillas son abstractas en ese sentido.

Por lo tanto, las plantillas nunca se compilan por separado y solo se compilan siempre 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, porque simplemente modificar el typename Tuso de un tipo concreto en el archivo .h no va a hacer el trabajo porque qué .cpp está allí para vincular, no puedo encontrarlo más adelante, porque las plantillas de recuerdo son abstractas y no se pueden compilar, por lo que me veo obligado a dar la implementación en este momento para saber qué compilar y vincular, y ahora que tengo la implementación, se vincula al archivo fuente adjunto. Básicamente, en el momento en que creo una plantilla, necesito crear una clase completamente nueva, y no puedo hacerlo si no sé cómo debería verse esa clase al usar el tipo que proporciono a menos que notifique al compilador de la implementación de la plantilla, por lo que ahora el compilador puede reemplazar Tcon 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 anular el resumen de las plantillas, por así decirlo, y lo hacemos dándoles un tipo concreto con el que lidiar para que nuestra abstracción de plantillas se pueda transformar en un archivo de clase normal y, a su vez, se pueda compilar normalmente. Separar el archivo .h de la plantilla y el archivo .cpp de la plantilla no tiene sentido. No tiene sentido porque la separación de .cpp y .h solo es donde el .cpp se puede compilar individualmente y enlazar individualmente, con plantillas, ya que no podemos compilarlas por separado, porque las plantillas son una abstracción, por lo tanto, siempre estamos obligados a coloque la abstracción siempre junto con la instanciación concreta donde la instanciación concreta siempre tiene que saber sobre el tipo que se está utilizando.

Lo que significa typename Tque se reemplaza durante el paso de compilación, no el paso de vinculación, por lo que si trato de compilar una plantilla sin Tser reemplazado como un tipo de valor concreto, eso no tiene ningún significado para el compilador y, como resultado, el código de objeto no se puede crear porque no lo hace. saber lo que Tes.

Es técnicamente posible crear algún tipo de funcionalidad que guarde el archivo template.cpp y cambie los tipos cuando los encuentre en otras fuentes, creo que el estándar tiene una palabra clave exportque le permitirá colocar plantillas en un lugar separado. 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 para un tipo concreto que se puede compilar y vincular individualmente.

4 Pranay May 13 2017 at 08:42

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 de reenvío. 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, después de lo cual se compila. De esta forma, no hay limitaciones y la nomenclatura es consistente, a cambio de un archivo extra.

Hago esto porque los analizadores estáticos para el código se rompen cuando no ven las declaraciones de clase hacia adelante en * .tpp. Esto es molesto cuando se escribe código en cualquier IDE o se usa YouCompleteMe u otros.

1 Juan Feb 18 2020 at 03:30

Sugiero mirar esta página de gcc que analiza las compensaciones entre el modelo "cfront" y "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 de plantilla completa y compilando las cosas varias veces.

Contiene recomendaciones explícitas sobre el uso de la instanciación de plantillas manual y automática. Por ejemplo, la opción "-repo" se puede utilizar para recopilar plantillas que necesitan instanciarse. Otra opción es deshabilitar las instancias de plantilla automáticas usando "-fno-implicit-templates" para forzar la instanciación de plantilla manual.

En mi experiencia, confío en que las plantillas C ++ Standard Library y Boost se instancian para cada unidad de compilación (usando una biblioteca de plantillas). Para mis clases de plantillas grandes, hago una 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 estuviera realmente preocupado por la velocidad, supongo que exploraría el uso de encabezados precompilados https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

que está ganando apoyo en muchos compiladores. Sin embargo, creo que los encabezados precompilados serían difíciles con los archivos de encabezado de plantilla.