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

1778

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).

ID principal
fuente
13
Si bien es cierto que colocar todas las definiciones de funciones de plantilla en el archivo de encabezado es probablemente la forma más conveniente de usarlas, todavía no está claro qué hace "en línea" en esa cita. No hay necesidad de usar funciones en línea para eso. "Inline" no tiene absolutamente nada que ver con esto.
ANT
77
El libro está desactualizado.
gerardw
1
Una plantilla no es como una función que se puede compilar en código de bytes. Es solo un patrón para generar dicha función. Si coloca una plantilla en un archivo * .cpp, no hay nada que compilar. Además, la instancia explícita en realidad no es una plantilla, sino el punto de partida para hacer una función de la plantilla que termina en el archivo * .obj.
dgrat
55
¿Soy el único que siente que el concepto de plantilla está paralizado en C ++ debido a esto? ...
DragonGamer

Respuestas:

1559

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:

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

// 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 las preguntas frecuentes de C ++ Super sobre este tema .

Luc Touraille
fuente
96
En realidad, la instanciación explícita debe estar en un archivo .cpp que tenga acceso a las definiciones para todas las funciones miembro de Foo, en lugar de en el encabezado.
Mankarse
11
"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" Pero, ¿por qué es una implementación en ¿El archivo .cpp no ​​es accesible para el compilador? Un compilador también puede acceder a la información .cpp, ¿de qué otra manera los convertiría en archivos .obj? EDITAR: la respuesta a esta pregunta está en el enlace proporcionado en esta respuesta ...
xcrypt
31
No creo que esto explique la pregunta de que claramente, la clave está obviamente relacionada con la unidad de compilación que no se menciona en esta publicación
zincó el
66
@Gabson: las estructuras y las clases son equivalentes con la excepción de que el modificador de acceso predeterminado para las clases es "privado", mientras que es público para las estructuras. Hay algunas otras pequeñas diferencias que puede aprender al observar esta pregunta .
Luc Touraille
3
Agregué una oración al comienzo de esta respuesta para aclarar que la pregunta se basa en una premisa falsa. Si alguien pregunta "¿Por qué X es verdad?" cuando de hecho X no es cierto, debemos rechazar rápidamente esa suposición.
Aaron McDaid
250

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.

template class vector<int>;

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.

MaHuJa
fuente
51
Ugh Buena respuesta, pero no hay una solución limpia real. Listado de todos los tipos posibles para una plantilla no parece ir con lo que se supone que es una plantilla.
Jiminion
66
Esto puede ser bueno en muchos casos, pero generalmente no cumple con el propósito de la plantilla, que le permite usar la clase con cualquiera typesin enumerarlos manualmente.
Tomáš Zato - Restablece a Mónica el
77
vectorno 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.
UncleZeiv
Se usa después de que se ha definido la plantilla, "y se han definido todas las funciones miembro". Gracias !
Vitt Volt
1
Siento que me falta algo ... Pongo la instanciación explícita para dos tipos en el .cpparchivo de la clase y las dos instancias se mencionan desde otros .cpparchivos, y todavía recibo el error de vinculación de que no se encuentran los miembros.
pez remo
250

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:

  • 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 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 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 uno 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 una MyClass<int>, pero no puede ver la plantilla MyClass<T>(solo su interfaz en foo.h ), por lo que no puede crearla.

Si usa foo.cppMyClass<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:

  • baz.cpp
    • declara e implementa class BazPrivate, y usaMyClass<BazPrivate>

No hay forma posible de que esto funcione a menos que

  1. Debe recompilar foo.cpp cada vez que cambiemos cualquier otro archivo en el programa , en caso de que agregue una nueva instancia deMyClass<T>
  2. Requiere que baz.cpp contenga (posiblemente mediante el encabezado incluye) la plantilla completa de MyClass<T>, de modo 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 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.

Ben
fuente
50
cita destacada 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 encontremos
v.oddou
Me gustaría saber, ¿es posible hacer instancias explícitas desde otro lugar que no sea el encabezado de la clase o el archivo fuente? Por ejemplo, ¿hacerlos en main.cpp?
gromit190
1
@Birger Debería poder hacerlo desde cualquier archivo que tenga acceso a la implementación completa de la plantilla (ya sea porque está en el mismo archivo o mediante el encabezado incluido).
Ben
11
@ajeh No es retórica. La pregunta es "¿por qué tiene que implementar plantillas en un encabezado?", Así que le expliqué las opciones técnicas que el lenguaje C ++ hace que conducen a este requisito. Antes de escribir mi respuesta, otros ya proporcionaron soluciones alternativas que no son soluciones completas, porque no puede haber una solución completa. Sentí que esas respuestas se complementarían con una discusión más completa del ángulo "por qué" de la pregunta.
Ben
1
Imagínelo de esta manera, amigos ... si no estuvieran usando plantillas (para codificar eficientemente lo que necesitaban), de todos modos solo ofrecerían algunas versiones de esa clase. entonces tienes 3 opciones. 1) No uses plantillas. (como todas las otras clases / funciones, a nadie le importa que otros no puedan alterar los tipos) 2). usar plantillas y documentar qué tipos pueden usar. 3) darles toda la bonificación de implementación (fuente) 4). darles toda la fuente en caso de que quieran hacer una plantilla de otra de sus clases;)
Charco
81

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, 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 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?).

David Hanak
fuente
1
"exportar" es estándar, pero es difícil de implementar, por lo que la mayoría de los equipos compiladores aún no lo han hecho.
vava
55
export no elimina la necesidad de revelar la fuente, ni reduce las dependencias de compilación, mientras que requiere un esfuerzo masivo por parte de los compiladores. Así que el propio Herb Sutter pidió a los creadores de compiladores que se 'olvidaran' de la exportación. Como el tiempo que se necesita invertir sería mejor gastarlo en otro lado ...
Pieter
2
Así que no creo que la exportación no se implemente 'todavía'. Probablemente nunca lo hará nadie más que EDG después de que los demás vieron cuánto tiempo tardó y qué poco se ganó
Pieter
3
Si eso le interesa, el documento se llama "¿Por qué no podemos permitirnos la exportación?", Aparece en su blog ( gotw.ca/publications ) pero no hay pdf allí (un rápido google debería aparecer)
Pieter
1
Ok, gracias por un buen ejemplo y explicación. Sin embargo, esta es mi pregunta: ¿por qué el compilador no puede averiguar dónde se llama la plantilla y compila esos archivos primero antes de compilar el archivo de definición? Me imagino que se puede hacer en un caso simple ... ¿Es la respuesta que las interdependencias estropearán el orden bastante rápido?
Vlad
62

En realidad, antes de C ++ 11, el estándar definía la exportpalabra 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 exportcaracterística de las plantillas con C ++ 11.

DevSolar
fuente
66
... y un par de años después, finalmente entendí lo exportque 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.
DevSolar
44
@DevSolar: este documento es político, repetitivo y está mal escrito. esa no es la prosa habitual de nivel estándar allí Innecesariamente largo y aburrido, que dice básicamente 3 veces lo mismo en decenas de páginas. Pero ahora estoy informado de que exportar no es exportar. Esa es una buena información!
v.oddou
1
@ v.oddou: Un buen desarrollador y un buen escritor técnico son dos habilidades separadas. Algunos pueden hacer ambas cosas, muchos no pueden. ;-)
DevSolar
@ v.oddou El documento no solo está mal escrito, es desinformación. También es un giro en la realidad: lo que en realidad son argumentos extremadamente fuertes para las exportaciones se mezclan de una manera que suena como si estuvieran en contra de la exportación: "descubrir numerosos agujeros relacionados con la ODR en el estándar en presencia de exportación. Antes de la exportación, el compilador no tenía que diagnosticar las violaciones de ODR. Ahora es necesario porque necesita combinar estructuras de datos internas de diferentes unidades de traducción, y no puede combinarlas si en realidad representan cosas diferentes, por lo que debe hacer la verificación ".
curioso
" ahora debe agregar en qué unidad de traducción estaba cuando sucedió " Duh. Cuando te ves obligado a usar argumentos poco convincentes, no tienes ningún argumento. Por supuesto que vas a mencionar nombres de archivos en tus errores, ¿cuál es el problema? Que alguien se enamore de ese BS es alucinante. " Incluso a expertos como James Kanze les resulta difícil aceptar que la exportación realmente sea así ".
curioso
34

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.

Anton Gogolev
fuente
¿Por qué no puedo implementarlos en el archivo .cpp con la palabra clave "en línea"?
MainID
2
Puedes, y no tienes que poner "en línea" incluso. Pero podría usarlos solo en ese archivo cpp y en ningún otro lugar.
vava
10
Esta es casi la respuesta más precisa , excepto que "eso significa que esos compiladores no permitirán que se definan en archivos que no sean encabezados, como archivos .cpp", es evidentemente falso.
Carreras de ligereza en órbita
28

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 .cpparchivo, 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 exportpalabra clave que debía usarse para una compilación separada. losexport característica está en desuso C++11y, AFAIK, solo un compilador la implementó. No deberías hacer uso de export. No es posible compilar por separado en C++o, C++11pero tal vez en C++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.

Germán Diago
fuente
15

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
    }
};
Benoît
fuente
15

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:

#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 forma, solo se deberán volver a compilar las instancias de la plantilla, no todos los usuarios de la plantilla (y sus dependencias).

lafrecciablu
fuente
1
Me gusta este enfoque con la excepción del MyInstantiatedTemplate.harchivo y el MyInstantiatedTemplatetipo 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/4612476
Cameron Tacklind
Esto toma lo mejor de dos mundos. ¡Ojalá esta respuesta tuviera una calificación más alta! También vea el enlace de arriba para una implementación ligeramente más limpia de la misma idea.
Wormer
8

Solo 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:

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;

    ...
}
Nikos
fuente
2
¿Para el hombre real? Si eso es cierto, entonces su respuesta debe verificarse como correcta. ¿Por qué alguien necesita todas esas cosas de vudú hacky si solo puede definir métodos que no sean miembros de plantilla en .cpp?
Michael IV
Bueno, eso no funciona. Al menos en MSVC 2019, obtener un símbolo externo sin resolver para una función miembro de la clase de plantilla.
Michael IV
No tengo MSVC 2019 para probar. Esto está permitido por el estándar C ++. Ahora, MSVC es conocido por no siempre cumplir con las reglas. Si aún no lo ha hecho, intente Configuración del proyecto -> C / C ++ -> Idioma -> Modo de conformidad -> Sí (permisivo-).
Nikos
1
Este ejemplo exacto funciona pero no puede llamar isEmptydesde ninguna otra unidad de traducción además de myQueue.cpp...
MM
7

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.

Eric Shaw
fuente
2
Esta respuesta debería modificarse bastante más. " Independientemente " descubrí su mismo enfoque y estaba buscando específicamente a alguien más que ya lo haya usado, ya que tengo curiosidad por saber si es un patrón oficial y si tiene un nombre. Mi enfoque es implementar un class XBasedonde sea que necesite implementar un template class X, colocando las partes dependientes del tipo Xy todo el resto XBase.
Fabio A.
6

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.

Robert
fuente
55
"los archivos de encabezado NO están compilados", esa es una forma realmente extraña de describirlo. Los archivos de encabezado pueden ser parte de una unidad de traducción, al igual que un archivo "c / cpp".
Flexo
2
De hecho, es casi lo contrario de la verdad, que es que los archivos de encabezado se compilan con mucha frecuencia muchas veces, mientras que un archivo de origen generalmente se compila una vez.
xaxxon
6

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 eltypename Tusar 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 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 que no tiene ningún significado para el compilador y, como resultado, el código objeto no se puede crear porque no saber lo que Tes

Es 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 exportque 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.

Moshe Rabaev
fuente
4

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

Pranay
fuente
2
s / inner_foo / foo / gy incluye foo.tpp al final de foo.h. Un archivo menos.
1

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.

Juan
fuente
-2

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:

template <class T>
T min(T const& one, T const& theOther);

Y en Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

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.

ClarHandsome
fuente
-7

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.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Ahora dentro de su archivo .template define sus funciones exactamente como lo haría normalmente.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }
Ni Nisan Nijackle
fuente
2
La mayoría de las personas definirían un archivo de encabezado como algo que propaga definiciones a los archivos de origen. Por lo tanto, puede haber decidido utilizar la extensión de archivo ".template" pero ha escrito un archivo de encabezado.
Tommy