Dividir clases de C ++ con plantilla en archivos .hpp / .cpp: ¿es posible?


96

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

$ 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;
}

ldes, por supuesto, correcto: los símbolos no están stack.o.

La respuesta a esta pregunta no ayuda, ya que estoy haciendo lo que dice.
Este podría ayudar, pero no quiero mover todos y cada uno de los métodos al .hpparchivo; no debería tener que hacerlo, ¿verdad?

¿Es la única solución razonable mover todo en el .cpparchivo al .hpparchivo y simplemente incluir todo, en lugar de vincularlo como un archivo de objeto independiente? ¡Eso parece terriblemente feo! En ese caso, yo también podría volver a mi estado anterior y el cambio de nombre stack.cppa stack.hppy hacerse con él.


Hay dos grandes soluciones para cuando realmente desea mantener su código oculto (en un archivo binario) o mantenerlo limpio. Es necesario reducir la generalidad aunque en la primera situación. Se explica aquí: stackoverflow.com/questions/495021/…
Sheric

Respuestas:


151

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 afirma, son soluciones para imitar el uso de un archivo cpp separado, pero prácticamente si tiene la intención de escribir una biblioteca de clases de plantilla y distribuirla con archivos de encabezado y lib para ocultar la implementación, simplemente no es posible .

Para saber por qué, veamos el proceso de compilación. Los archivos de encabezado nunca se compilan. Solo están preprocesados. El código preprocesado se combina 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 en absoluto, 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. Mientras 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 cambios de nombre y el primer parámetro como el objeto sobre el que opera. El argumento 'this' es el que realmente indica el tamaño del objeto que, en caso de que la 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 del método en un archivo cpp separado e intenta compilarlo, el archivo objeto en sí no se generará con la información de la clase. La compilación no fallará, generará el archivo de objeto pero no generará ningún código para la clase de plantilla en el archivo de objeto. Esta es la razón por la que el vinculador no puede encontrar los símbolos en los archivos de objeto y la compilación falla.

Ahora bien, ¿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 la implementación en forma binaria. Aquí es donde debe separar las estructuras de datos y los algoritmos. Las clases de su plantilla deben representar solo estructuras de datos, no algoritmos. Esto le permite ocultar detalles de implementación más valiosos en bibliotecas de clases separadas sin plantilla, las clases dentro de las cuales funcionarí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 configurar datos. El resto del trabajo lo realizarían las clases de algoritmos.

Espero que esta discusión sea útil.


2
"Debe entenderse que la clase de plantilla no es una clase en absoluto", ¿no fue al revés? La plantilla de clase es una plantilla. La "clase de plantilla" se usa a veces en lugar de la "instanciación de una plantilla", y sería una clase real.
Xupicor

¡Solo como referencia, no es correcto decir que no hay soluciones! Separar las estructuras de datos de los métodos también es una mala idea, ya que se opone a la encapsulación. Hay una gran solución alternativa que puede usar en algunas situaciones (creo que la mayoría) aquí: stackoverflow.com/questions/495021/…
Sheric

@Xupicor, tienes razón. Técnicamente, "Plantilla de clase" es lo que escribe para poder crear una instancia de una "Clase de plantilla" y su objeto correspondiente. Sin embargo, creo que en una terminología genérica, usar ambos términos indistintamente no sería tan incorrecto, la sintaxis para definir la "Plantilla de clase" en sí misma comienza con la palabra "plantilla" y no "clase".
Sharjith N.

@Sheric, no dije que no hay soluciones. De hecho, todo lo que está disponible son solo soluciones para imitar la separación de interfaz e implementación en el caso de clases de plantilla. Ninguna de esas soluciones funciona sin crear una instancia de una clase de plantilla especificada. De todos modos, eso disuelve todo el punto de genérico del uso de plantillas de clase. Separar estructuras de datos de algoritmos no es lo mismo que separar estructuras de datos de métodos. Las clases de estructura de datos pueden tener métodos como constructores, captadores y definidores.
Sharjith N.

Lo más parecido que encontré para hacer que esto funcione es usar un par de archivos .h / .hpp, y #incluir "filename.hpp" al final del archivo .h que define su clase de plantilla. (debajo de la llave de cierre para la definición de clase con punto y coma). Esto al menos los separa estructuralmente por archivo, y está permitido porque al final, el compilador copia / pega su código .hpp sobre su #include "filename.hpp".
Artorias2718

90

Que es posible, siempre y cuando usted sabe lo ejemplificaciones que vas a necesidad.

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 de pila que no sean de plantilla y el paso de enlace funcionará bien.


7
En la práctica, la mayoría de la gente usa un archivo cpp separado para esto, algo como stackinstantiations.cpp.
Nemanja Trifunovic

@NemanjaTrifunovic, ¿puedes dar un ejemplo de cómo se vería stackinstantiations.cpp?
qwerty9967

3
En realidad, hay otras soluciones: codeproject.com/Articles/48575/…
sleepsort

@ Benoît Recibí un error de error: esperaba unqualified-id antes de ';' pila de plantillas de tokens <int>; ¿Sabes por qué? ¡Gracias!
camino

3
En realidad, la sintaxis correcta es template class stack<int>;.
Paul Baltescu

8

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 se ha discutido en Daniweb

También en las preguntas frecuentes pero usando la palabra clave de exportación C ++.


5
includeLa creación de un cpparchivo es generalmente una idea terrible. incluso si tiene una razón válida para esto, el archivo, que en realidad es solo un encabezado glorificado, debe recibir una hppextensión o alguna diferente (por ejemplo tpp) para dejar muy claro lo que está sucediendo, eliminar la confusión sobre los archivos que se makefiledirigen a archivos reales cpp , etc.
underscore_d

@underscore_d ¿Podría explicar por qué incluir un .cpparchivo es una idea terrible?
Abbas

1
@Abbas porque la extensión cpp(o cc, o c, o lo que sea) indica que el archivo es una parte de la implementación, que la unidad de traducción resultante (salida del preprocesador) se puede compilar por separado y que el contenido del archivo se compila una sola vez. no indica que el archivo sea una parte reutilizable de la interfaz, para ser incluido arbitrariamente en cualquier lugar. #includeLa creación de un archivo real cpp llenaría rápidamente su pantalla con múltiples errores de definición, y con razón. en este caso, ya que no es una razón para #includeello, cppera sólo la elección equivocada de extensión.
underscore_d

@underscore_d Entonces, básicamente, es incorrecto usar la .cppextensión para tal uso. Pero usar otra palabra .tppestá completamente bien, ¿cuál serviría para el mismo propósito pero usaría una extensión diferente para una comprensión más fácil / rápida?
Abbas

1
@Abbas Sí, cpp/ cc/ etc es necesario evitar, pero es una idea buena para usar algo distinto hpp- por ejemplo tpp, tcc, etc - para que pueda volver a utilizar el resto del nombre de archivo e indicar que el tpparchivo, aunque actúa como un encabezado, contiene la implementación fuera de línea de las declaraciones de plantilla en el correspondiente hpp. Entonces, esta publicación comienza con una buena premisa: separa las declaraciones y definiciones en 2 archivos diferentes, que pueden ser más fáciles de asimilar / grep o, a veces, se requieren debido a dependencias circulares IME, pero luego termina mal sugiriendo que el segundo archivo tiene una extensión incorrecta
underscore_d

6

No, no es posible. No sin la exportpalabra clave, que para todos los efectos no existe realmente.

Lo mejor que puede hacer es poner las implementaciones de sus funciones en un archivo ".tcc" o ".tpp" y #incluya 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.


3
Tu respuesta no es correcta. Puede generar código a partir de una clase de plantilla en un archivo cpp, siempre que sepa qué argumentos de plantilla utilizar. Consulte mi respuesta para obtener más información.
Benoît

2
Es cierto, pero esto viene con la seria restricción de tener que actualizar el archivo .cpp y recompilar cada vez que se introduce un nuevo tipo que usa la plantilla, que probablemente no sea lo que el OP tenía en mente.
Charles Salvia

3

Creo que hay dos razones principales para intentar separar el código con 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 reutilizable más tarde.

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

Actualmente (como siempre) estoy codificando software de simulación en conjunto con OpenCL y nos gusta mantener el código para que pueda ejecutarse usando los tipos float (cl_float) o double (cl_double) según sea necesario, dependiendo de la capacidad de HW. Ahora mismo esto se hace usando un #define REAL al principio del código, pero esto no es muy elegante. Cambiar la precisión deseada requiere volver a compilar la aplicación. Dado que no hay tipos de tiempo de ejecución reales, tenemos que vivir con esto por el momento. Afortunadamente, los núcleos OpenCL se compilan en tiempo de ejecución, y un tamaño simple de (REAL) nos permite alterar el tiempo de ejecución del código del núcleo en consecuencia.

El problema mucho mayor es que a pesar de que la aplicación es modular, al desarrollar clases auxiliares (como las que precalculan las constantes de simulación) también hay que crear plantillas. Todas estas clases aparecen al menos una vez en la parte superior del árbol de dependencia de clases, ya que la clase de plantilla final Simulation tendrá una instancia de una de estas clases de fábrica, lo que significa que prácticamente cada vez que hago un cambio menor en la clase de fábrica, todo el software debe reconstruirse. Esto es muy molesto, pero parece que no puedo encontrar una solución mejor.


2

Solo si #include "stack.cppal final de stack.hpp. Solo recomendaría este enfoque si la implementación es relativamente grande y si cambia el nombre del archivo .cpp a otra extensión, para diferenciarlo del código normal.


4
Si está haciendo esto, querrá agregar #ifndef STACK_CPP (y amigos) a su archivo stack.cpp.
Stephen Newell

Gáname con esta sugerencia. Yo tampoco prefiero este enfoque por razones de estilo.
Lucas

2
Sí, en tal caso, el segundo archivo definitivamente no debería tener la extensión cpp( cco lo que sea) porque eso es un marcado contraste con su función real. En su lugar, debería recibir una extensión diferente que indique que es (A) un encabezado y (B) un encabezado que se incluirá en la parte inferior de otro encabezado. Yo utilizo tpppara esto, que también puede significar fácilmente implementación tem plate p(definiciones fuera de línea). Divagué más sobre esto aquí: stackoverflow.com/questions/1724036/…
underscore_d

2

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


+1 - aunque no funciona muy bien la mayor parte del tiempo (al menos, no tan a menudo como deseo)
peterchen

2

Si sabe con qué tipos se utilizará su pila, puede instanciarlos explícitamente en el archivo cpp y mantener allí todo el código relevante.

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

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


2

El problema es que una plantilla no genera una clase real, es solo una plantilla que le dice al compilador cómo generar una clase. Necesitas generar una clase concreta.

La forma 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 instanciación de plantilla y método que necesita, el compilador las generará allí para usarlas en todo su 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
}

8
No necesita la función dummey: use 'template stack <int>;' Esto fuerza una instanciación de la plantilla en la unidad de compilación actual. Muy útil si define una plantilla pero solo desea un par de implementaciones específicas en una biblioteca compartida.
Martin York

@Martin: ¿incluidas todas las funciones de los miembros? Eso es fantástico. Debe agregar esta sugerencia al hilo "características ocultas de C ++".
Mark Ransom

@LokiAstari Encontré un artículo sobre esto en caso de que alguien quiera aprender más: cplusplus.com/forum/articles/14272
Andrew Larsson

1

Necesita tener todo en el archivo hpp. El problema es que las clases no se crean realmente hasta que el compilador ve que algún OTRO archivo cpp las necesita, por lo que debe tener todo el código disponible para compilar la clase con 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 plantilla específica del tipo que hereda la clase sin plantilla.


0

Debido a que las plantillas se compilan cuando es necesario, esto obliga a una restricción para proyectos de varios archivos: la implementación (definición) de una clase o función de plantilla debe estar en el mismo archivo que su declaración. Eso significa que no podemos separar la interfaz en un archivo de encabezado separado y que debemos incluir tanto la interfaz como la implementación en cualquier archivo que use las plantillas.


0

Otra posibilidad es hacer algo como:

#ifndef _STACK_HPP
#define _STACK_HPP

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

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

No me gusta esta sugerencia por cuestión de estilo, pero puede que le convenga.


1
El segundo encabezado glorificado que se incluye debe tener al menos una extensión que no sea cpppara evitar confusión con los archivos fuente reales . Las sugerencias comunes incluyen tppy tcc.
underscore_d

0

La palabra clave 'exportar' es la forma de separar la implementación de la plantilla de la declaración de la plantilla. Esto se introdujo en el estándar C ++ sin una implementación existente. A su debido tiempo, solo un par de compiladores lo implementaron. Lea información detallada en el artículo de Inform IT sobre exportación


1
Esta es casi una respuesta de solo enlace, y ese enlace está muerto.
undercore_d

0

1) Recuerde que la razón principal para separar los archivos .hy .cpp es ocultar la implementación de la clase como un código Obj compilado por separado que se puede vincular al código del usuario que incluye un .h de la clase.

2) Las clases que no son de plantilla tienen todas las variables definidas concreta y específicamente en archivos .hy .cpp. Por lo tanto, el compilador tendrá la información necesaria sobre todos los tipos de datos utilizados en la clase antes de compilar / traducir  generar el código de objeto / máquina Las clases de plantilla no tienen información sobre el tipo de datos específico antes de que el usuario de la clase instancia un objeto pasando los datos requeridos tipo:

        TClass<int> myObj;

3) Solo después de esta instanciación, el compilador genera la versión específica de la clase de plantilla para que coincida con los tipos de datos pasados.

4) Por lo tanto, .cpp NO se puede compilar por separado sin conocer el tipo de datos específico del usuario. Por lo tanto, debe permanecer como código fuente dentro de ".h" hasta que el usuario especifique el tipo de datos requerido, luego, se puede generar a un tipo de datos específico y luego se compila


0

El lugar donde podría querer hacer esto es cuando crea una combinación de biblioteca y encabezado, y oculta la implementación al usuario. Por lo tanto, el enfoque sugerido es utilizar la instanciación explícita, porque sabe lo que se espera que proporcione su software y puede ocultar las implementaciones.

Aquí hay información útil: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Para su mismo ejemplo: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Salida:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

Sin embargo, no me gusta del todo este enfoque, porque permite que la aplicación se dispare en el pie, pasando tipos de datos incorrectos a la clase de plantilla. Por ejemplo, en la función principal, puede pasar otros tipos que se pueden convertir implícitamente a int como s.Push (1.2); y eso es simplemente malo en mi opinión.


-3

Estoy trabajando con Visual Studio 2010, si desea dividir sus archivos en .hy .cpp, incluya su encabezado cpp al final del archivo .h

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.