Pregunta División de clases de C ++ con plantillas en archivos .hpp / .cpp: ¿es posible?


Recibo errores al intentar compilar una clase de plantilla de C ++ que se divide entre .hpp y .cpp archivo:

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

Aquí está mi código:

stack.hpp:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp:

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp:

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ld es por supuesto correcto: los símbolos no están en stack.o.

La respuesta a esta pregunta no ayuda, como ya estoy haciendo como dice.
Éste podría ayudar, pero no quiero mover todos los métodos en el .hpp archivo: no debería tener que hacerlo, ¿debería?

Es la única solución razonable para mover todo en el .cpp archivo a la .hpp archivo, y simplemente incluir todo, en lugar de vincular como un archivo de objeto independiente? Eso parece muy ¡feo! En ese caso, también podría volver a mi estado anterior y cambiar el nombre stack.cpp a stack.hpp y termine con esto.


73
2017-11-12 17:40


origen


Respuestas:


No es posible escribir la implementación de una clase de plantilla en un archivo cpp separado y compilar. Todas las formas de hacerlo, si alguien reclama, son soluciones para imitar el uso de archivos cpp separados, pero prácticamente si tiene la intención de escribir una biblioteca de clases de plantillas y distribuirlas con archivos de cabecera y lib para ocultar la implementación, simplemente no es posible .

Para saber por qué, echemos un vistazo al proceso de compilación. Los archivos de encabezado nunca se compilan. Solo están preprocesados. El código preprocesado se golpea con el archivo cpp que en realidad se compila. Ahora, si el compilador tiene que generar el diseño de memoria apropiado para el objeto, necesita conocer el tipo de datos de la clase de plantilla.

En realidad, debe entenderse que la clase de plantilla no es una clase sino una plantilla para una clase cuya declaración y definición es generada por el compilador en el momento de la compilación después de obtener la información del tipo de datos del argumento. Siempre que no se pueda crear el diseño de la memoria, no se pueden generar las instrucciones para la definición del método. Recuerde que el primer argumento del método de clase es el operador 'this'. Todos los métodos de clase se convierten en métodos individuales con el cambio de nombre y el primer parámetro como el objeto sobre el que opera. El argumento "this" es el que realmente informa sobre el tamaño del objeto que en caso de clase de plantilla no está disponible para el compilador a menos que el usuario cree una instancia del objeto con un argumento de tipo válido. En este caso, si coloca las definiciones de método en un archivo cpp independiente e intenta compilarlo, el archivo de objeto en sí no se generará con la información de la clase. La compilación no fallará, generará el archivo objeto pero no generará ningún código para la clase de plantilla en el archivo objeto. Esta es la razón por la cual el enlazador no puede encontrar los símbolos en los archivos del objeto y falla la compilación.

Ahora, ¿cuál es la alternativa para ocultar detalles importantes de implementación? Como todos sabemos, el objetivo principal detrás de separar la interfaz de la implementación es ocultar los detalles de implementación en forma binaria. Aquí es donde debe separar las estructuras de datos y los algoritmos. Sus clases de plantilla deben representar solo estructuras de datos, no los algoritmos. Esto le permite ocultar detalles de implementación más valiosos en bibliotecas de clases no templadas independientes, las clases dentro de las cuales trabajarían en las clases de plantilla o simplemente las usarían para almacenar datos. La clase de plantilla en realidad contendría menos código para asignar, obtener y establecer datos. El resto del trabajo lo harían las clases de algoritmo.

Espero que esta discusión sea útil.


131
2018-01-26 23:52



Eso es posible, siempre que sepa qué instancias necesitará.

Agregue el siguiente código al final de stack.cpp y funcionará:

template class stack<int>;

Se crearán instancias de todos los métodos que no sean de plantilla y el paso de vinculación funcionará correctamente.


74
2017-11-12 19:13



Puedes hacerlo de esta manera

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Esto ha sido discutido en Daniweb

También en Preguntas más frecuentes pero usando la palabra clave de exportación C ++.


8
2018-04-03 12:06



No, no es posible. No sin el export palabra clave, que para todos los efectos no existe realmente.

Lo mejor que puede hacer es colocar las implementaciones de su función en un archivo ".tcc" o ".tpp" y #incluir el archivo .tcc al final de su archivo .hpp. Sin embargo, esto es meramente cosmético; sigue siendo lo mismo que implementar todo en los archivos de encabezado. Este es simplemente el precio que paga por usar plantillas.


6
2017-11-12 17:41



Creo que hay dos razones principales para tratar de separar el código de plantilla en un encabezado y un cpp:

Uno es por mera elegancia. A todos nos gusta escribir código que sea fácil de leer, administrar y pueda volver a usarse más adelante.

Otro es la reducción de los tiempos de compilación.

Actualmente estoy (como siempre) programando software de simulación en conjunción con OpenCL y nos gusta mantener el código para que pueda ejecutarse utilizando los tipos float (cl_float) o double (cl_double) según sea necesario, dependiendo de la capacidad de HW. En este momento esto se hace usando un #define REAL al comienzo del código, pero esto no es muy elegante. Cambiar la precisión deseada requiere recompilar la aplicación. Como no hay tipos reales de tiempo de ejecución, tenemos que vivir con esto por el momento. Afortunadamente, los kernels OpenCL son compilados en tiempo de ejecución, y un tamaño simple de (REAL) nos permite alterar el tiempo de ejecución del código kernel en consecuencia.

El problema mucho más grande es que, aunque la aplicación es modular, al desarrollar clases auxiliares (como las que calculan previamente las constantes de simulación) también deben ser modeladas. Todas estas clases aparecen al menos una vez en la parte superior del árbol de dependencias de clase, ya que la clase de plantilla final Simulación tendrá una instancia de una de estas clases de fábrica, lo que significa que prácticamente cada vez que hago un cambio menor a la clase de fábrica, todo el software tiene que ser reconstruido. Esto es muy molesto, pero parece que no puedo encontrar una mejor solución.


3
2017-11-09 09:05



A veces es posible tener la mayor parte de la implementación escondida en el archivo cpp, si puede extraer la funcionalidad común para todos los parámetros de la plantilla en una clase que no sea de plantilla (posiblemente escriba-inseguro). Entonces, el encabezado contendrá llamadas de redirección a esa clase. Se usa un enfoque similar cuando se combate con el problema de "hinchazón de plantilla".


2
2017-11-12 18:07



Si sabe con qué tipos se utilizará su pila, puede instanciarlos explícitamente en el archivo cpp y conservar allí todos los códigos relevantes.

También es posible exportarlos a través de DLL (!) Pero es bastante complicado obtener la sintaxis correcta (combinaciones MS específicas de __declspec (dllexport) y la palabra clave export).

Lo hemos usado en una matemática / geom lib que tiene la plantilla doble / flotante, pero tenía bastante código. (Lo busqué en Google en ese momento, aunque no tengo ese código hoy).


2
2017-11-12 18:16



El problema es que una plantilla no genera una clase real, es solo una modelo diciéndole al compilador cómo generar una clase. Necesitas generar una clase concreta.

La manera fácil y natural es poner los métodos en el archivo de encabezado. Pero hay otra manera.

En su archivo .cpp, si tiene una referencia a cada instancia de plantilla y método que necesita, el compilador los generará allí para su uso en todo el proyecto.

nuevo stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

2
2017-11-12 18:11



Necesitas tener todo en el archivo hpp. El problema es que las clases no se crean realmente hasta que el compilador ve que las necesita algún otro archivo cpp, por lo que debe tener todo el código disponible para compilar la clase de plantilla en ese momento.

Una cosa que tiendo a hacer es tratar de dividir mis plantillas en una parte genérica sin plantilla (que se puede dividir entre cpp / hpp) y la parte de la plantilla específica del tipo que hereda la clase sin plantilla.


1
2017-11-12 17:44