archivos de cabecera en dev c++




¿Por qué solo se pueden implementar plantillas en el archivo de encabezado? (10)

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.


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>
  • foo.cpp
    • Define la implementación de la class MyClass<T>
  • bar.cpp
    • utiliza 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. 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 utiliza MyClass<BazPrivate>

No hay forma posible de que esto funcione a menos que nosotros

  1. 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>
  2. Requerir que baz.cpp contenga (posiblemente a través del encabezado incluye) 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 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.


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


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 .


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.





c++-faq