Almacenar definiciones de funciones de plantilla C ++ en un archivo .CPP

526

Tengo un código de plantilla que preferiría haber almacenado en un archivo CPP en lugar de en línea en el encabezado. Sé que esto se puede hacer siempre que sepa qué tipos de plantillas se utilizarán. Por ejemplo:

archivo .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

archivo .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Tenga en cuenta las dos últimas líneas: la función de plantilla foo :: do solo se usa con ints y std :: strings, por lo que esas definiciones significan que la aplicación se vinculará.

Mi pregunta es: ¿es un truco desagradable o funcionará con otros compiladores / enlazadores? Solo estoy usando este código con VS2008 en este momento, pero querré portar a otros entornos.

Robar
fuente
22
No tenía idea de que esto era posible, ¡un truco interesante! Hubiera ayudado a algunas tareas recientes considerables para saber esto - ¡salud!
xan
69
Lo que me pisotea es el uso de docomo identificador: p
Quentin
He hecho algo similar con gcc, pero todavía estoy investigando
Nick
16
Esto no es un "hack", es una declinación hacia adelante. Esto tiene un lugar en el estándar del idioma; así que sí, está permitido en todos los compiladores conformes estándar.
Ahmet Ipkin
1
¿Qué pasa si tienes docenas de métodos? ¿Se puede hacer template class foo<int>;template class foo<std::string>;al final del archivo .cpp?
Ignorante

Respuestas:

231

El problema que describe puede resolverse definiendo la plantilla en el encabezado o mediante el enfoque que describió anteriormente.

Recomiendo leer los siguientes puntos de C ++ FAQ Lite :

Entran en muchos detalles sobre estos (y otros) problemas de plantilla.

Aaron N. Tubbs
fuente
39
Solo para complementar la respuesta, el enlace al que se hace referencia responde positivamente a la pregunta, es decir, es posible hacer lo que Rob sugirió y que el código sea portátil.
ivotron
161
¿Puedes publicar las partes relevantes en la respuesta misma? ¿Por qué esta referencia incluso está permitida en SO? No tengo idea de qué buscar en este enlace, ya que ha cambiado mucho desde entonces.
Ident
124

Para otros en esta página que se preguntan cuál es la sintaxis correcta (como lo hice yo) para la especialización explícita de plantillas (o al menos en VS2008), es la siguiente ...

En su archivo .h ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

Y en tu archivo .cpp

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
sid de espacio de nombres
fuente
15
¿Quiere decir "para la especialización explícita de la plantilla CLASS" En ese caso, ¿eso abarcará todas las funciones que tiene la clase con plantilla?
Arthur
@Arthur parece que no, tengo algunos métodos de plantilla que permanecen en el encabezado y la mayoría de los otros métodos en cpp, funciona bien. Muy buena solución.
user1633272
En el caso del autor de la pregunta, tienen una plantilla de función, no una plantilla de clase.
user253751
23

Este código está bien formado. Solo tiene que prestar atención a que la definición de la plantilla sea visible en el punto de creación de instancias. Para citar el estándar, § 14.7.2.4:

La definición de una plantilla de función no exportada, una plantilla de función miembro no exportada, o una función miembro no exportada o miembro de datos estáticos de una plantilla de clase debe estar presente en cada unidad de traducción en la que se instancia explícitamente.

Konrad Rudolph
fuente
2
¿Qué significa no exportado ?
Dan Nissenbaum
1
@Dan Visible solo dentro de su unidad de compilación, no fuera de ella. Si vincula varias unidades de compilación, los símbolos exportados se pueden usar a través de ellos (y deben tener una sola, o al menos, en el caso de plantillas, definiciones consistentes, de lo contrario, se encontrará con UB).
Konrad Rudolph
Gracias. Pensé que todas las funciones son (por defecto) visibles fuera de la unidad de compilación. Si tengo dos unidades de compilación a.cpp(definiendo la función a() {}) y b.cpp(definiendo la función b() { a() }), esto se vinculará con éxito. Si estoy en lo cierto, entonces la cita anterior parecería no aplicarse para el caso típico ... ¿Me estoy equivocando en alguna parte?
Dan Nissenbaum
@Dan contraejemplo trivial: inlinefunciones
Konrad Rudolph
1
Las plantillas de la función @Dan están implícitamente inline. La razón es que sin un ABI C ++ estandarizado es difícil / imposible definir el efecto que esto tendría de otra manera.
Konrad Rudolph
15

Esto debería funcionar bien en todas las plantillas compatibles. La creación de instancias de plantilla explícita es parte del estándar C ++.

sombra de Luna
fuente
13

Su ejemplo es correcto pero no muy portátil. También hay una sintaxis un poco más limpia que se puede usar (como lo señala @ namespace-sid).

Supongamos que la clase con plantilla es parte de alguna biblioteca que se va a compartir. ¿Deberían compilarse otras versiones de la clase con plantilla? ¿Se supone que el mantenedor de la biblioteca anticipa todos los posibles usos de plantilla de la clase?

Un enfoque alternativo es una ligera variación de lo que tiene: agregue un tercer archivo que es el archivo de implementación / instanciación de plantilla.

archivo foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

archivo foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

archivo foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

La única advertencia es que necesita decirle al compilador que compile en foo-impl.cpplugar de hacerlo, foo.cppya que la compilación no hace nada.

Por supuesto, puede tener múltiples implementaciones en el tercer archivo o tener múltiples archivos de implementación para cada tipo que le gustaría usar.

Esto permite mucha más flexibilidad al compartir la clase de plantilla para otros usos.

Esta configuración también reduce los tiempos de compilación para las clases reutilizadas porque no está volviendo a compilar el mismo archivo de encabezado en cada unidad de traducción.

Cameron Tacklind
fuente
¿Qué te compra esto? Aún necesita editar foo-impl.cpp para agregar una nueva especialización.
MK.
Separación de los detalles de implementación (también conocidos como definiciones en foo.cpp) de las versiones que realmente se compilan (en foo-impl.cpp) y declaraciones (en foo.h). No me gusta que la mayoría de las plantillas de C ++ se definan completamente en archivos de encabezado. Eso es contrario al estándar C / C ++ de pares de c[pp]/hpara cada clase / espacio de nombres / cualquier agrupación que use. Parece que las personas todavía usan archivos de encabezado monolíticos simplemente porque esta alternativa no se usa ni se conoce ampliamente.
Cameron Tacklind
1
@MK. Estaba colocando las instancias de plantilla explícitas al final de la definición en el archivo fuente al principio hasta que necesité más instancias en otro lugar (por ejemplo, pruebas unitarias utilizando un simulacro como tipo de plantilla). Esta separación me permite agregar más instancias externamente. Además, todavía funciona cuando mantengo el original como un h/cpppar, aunque tuve que rodear la lista original de instancias en un protector de inclusión, pero aún podía compilar el foo.cppnormal. Sin embargo, todavía soy bastante nuevo en C ++ y me interesaría saber si este uso mixto tiene alguna advertencia adicional.
Thirdwater
3
Creo que es preferible desacoplar foo.cppy foo-impl.cpp. No #include "foo.cpp"en el foo-impl.cpparchivo; en su lugar, agregue la declaración extern template class foo<int>;para foo.cppevitar que el compilador cree instancias de la plantilla al compilar foo.cpp. Asegúrese de que el sistema de compilación compila ambos .cpparchivos y pasa ambos archivos de objeto al vinculador. Esto tiene múltiples beneficios: a) está claro en foo.cppque no hay instanciación; b) los cambios en foo.cpp no ​​requieren una compilación de foo-impl.cpp.
Shmuel Levine
3
Este es un enfoque muy bueno para el problema de las definiciones de plantillas que toma lo mejor de ambos mundos: implementación de encabezado e instanciación para tipos de uso frecuente. El único cambio que haría a esta configuración es para cambiar el nombre foo.cppen foo_impl.hy foo-impl.cppen solo foo.cpp. También agregaría typedefs para instancias de foo.cppa foo.h, del mismo modo using foo_int = foo<int>;. El truco es proporcionar a los usuarios dos interfaces de encabezado para elegir. Cuando el usuario necesita una instanciación predefinida que incluye foo.h, cuando el usuario necesita algo fuera de orden que incluye foo_impl.h.
Wormer
5

Esto definitivamente no es un truco desagradable, pero tenga en cuenta el hecho de que tendrá que hacerlo (la especialización explícita de la plantilla) para cada clase / tipo que desee utilizar con la plantilla dada. En el caso de MUCHOS tipos que solicitan la creación de instancias de plantilla, puede haber MUCHAS líneas en su archivo .cpp. Para solucionar este problema, puede tener un TemplateClassInst.cpp en cada proyecto que use para que tenga un mayor control sobre los tipos que se instanciarán. Obviamente, esta solución no será perfecta (también conocida como bala de plata), ya que podría terminar rompiendo el ODR :).

Rojo XIII
fuente
¿Estás seguro de que romperá la ODR? Si las líneas de creación de instancias en TemplateClassInst.cpp se refieren al archivo fuente idéntico (que contiene las definiciones de función de plantilla), ¿no se garantiza que no violará el ODR ya que todas las definiciones son idénticas (incluso si se repiten)?
Dan Nissenbaum
Por favor, ¿qué es ODR?
extraíble el
4

Hay, en el último estándar, una palabra clave (export ) que ayudaría a aliviar este problema, pero no está implementado en ningún compilador que conozca, aparte de Comeau.

Consulte las preguntas frecuentes sobre esto.

Ben Collins
fuente
2
AFAIK, la exportación está muerta porque se enfrentan a problemas cada vez más nuevos, cada vez que resuelven el último, lo que hace que la solución general sea cada vez más complicada. Y la palabra clave "exportar" no le permitirá "exportar" de un CPP de todos modos (aún de H. Sutter). Por eso digo: No contenga la respiración ...
paercebal
2
Para implementar la exportación, el compilador todavía requiere la definición de plantilla completa. Todo lo que gana es tenerlo en una especie de forma compilada. Pero realmente no tiene sentido.
Zan Lynx
2
... y se ha pasado del estándar, debido a una complicación excesiva para una ganancia mínima.
DevSolar
4

Esa es una forma estándar de definir funciones de plantilla. Creo que hay tres métodos que leo para definir plantillas. O probablemente 4. Cada uno con pros y contras.

  1. Definir en la definición de clase. No me gusta para nada porque creo que las definiciones de clase son estrictamente de referencia y deberían ser fáciles de leer. Sin embargo, es mucho menos complicado definir plantillas en clase que fuera. Y no todas las declaraciones de plantilla están en el mismo nivel de complejidad. Este método también hace que la plantilla sea una plantilla verdadera.

  2. Defina la plantilla en el mismo encabezado, pero fuera de la clase. Esta es mi forma preferida la mayoría de las veces. Mantiene su definición de clase ordenada, la plantilla sigue siendo una plantilla verdadera. Sin embargo, requiere un nombre completo de la plantilla que puede ser complicado. Además, su código está disponible para todos. Pero si necesita que su código esté en línea, esta es la única manera. También puede lograr esto creando un archivo .INL al final de sus definiciones de clase.

  3. Incluya el header.h y la implementación.CPP en su main.CPP. Creo que así es como se hace. No tendrá que preparar ninguna instancia previa, se comportará como una plantilla verdadera. El problema que tengo es que no es natural. Normalmente no incluimos y esperamos incluir archivos de origen. Supongo que dado que incluiste el archivo fuente, las funciones de la plantilla se pueden incorporar.

  4. Este último método, que era la forma publicada, es definir las plantillas en un archivo fuente, al igual que el número 3; pero en lugar de incluir el archivo fuente, crearemos una instancia previa de las plantillas que necesitaremos. No tengo ningún problema con este método y a veces resulta útil. Tenemos un código grande, no puede beneficiarse de estar en línea, así que solo póngalo en un archivo CPP. Y si conocemos instancias comunes y podemos predefinirlas. Esto nos salva de escribir básicamente lo mismo 5, 10 veces. Este método tiene la ventaja de mantener nuestro código patentado. Pero no recomiendo poner pequeñas funciones de uso regular en los archivos CPP. Como esto reducirá el rendimiento de su biblioteca.

Tenga en cuenta que no conozco las consecuencias de un archivo obj hinchado.

Cássio Renan
fuente
3

Sí, esa es la forma estándar de especialización. instanciación explícita. Como indicó, no puede crear una instancia de esta plantilla con otros tipos.

Editar: corregido en función del comentario.

Lou Franco
fuente
Ser exigente con la terminología es una "instanciación explícita".
Richard Corden el
2

Tomemos un ejemplo, digamos por alguna razón que desea tener una clase de plantilla:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Si compila este código con Visual Studio, funciona de forma inmediata. gcc producirá un error de enlazador (si se usa el mismo archivo de encabezado de múltiples archivos .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Es posible mover la implementación al archivo .cpp, pero luego debe declarar una clase como esta:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

Y luego .cpp se verá así:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Sin dos últimas líneas en el archivo de encabezado, gcc funcionará bien, pero Visual Studio producirá un error:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

la sintaxis de clase de plantilla es opcional en caso de que desee exponer la función a través de la exportación .dll, pero esto es aplicable solo para la plataforma de Windows, por lo que test_template.h podría verse así:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

con el archivo .cpp del ejemplo anterior.

Sin embargo, esto le da más dolor de cabeza al enlazador, por lo que se recomienda usar el ejemplo anterior si no exporta la función .dll.

TarmoPikaro
fuente
1

¡Hora de una actualización! Cree un archivo en línea (.inl, o probablemente cualquier otro) y simplemente copie todas sus definiciones en él. Asegúrese de agregar la plantilla sobre cada función ( template <typename T, ...>). Ahora, en lugar de incluir el archivo de encabezado en el archivo en línea, haga lo contrario. Incluya el archivo en línea después de la declaración de su clase ( #include "file.inl").

Realmente no sé por qué nadie ha mencionado esto. No veo inconvenientes inmediatos.

Didii
fuente
25
Los inconvenientes inmediatos es que es básicamente lo mismo que definir las funciones de la plantilla directamente en el encabezado. Una vez que usted #include "file.inl", el preprocesador va a pegar el contenido file.inldirectamente en el encabezado. Cualquiera sea la razón por la que deseaba evitar la implementación en el encabezado, esta solución no resuelve ese problema.
Cody Gray
55
- y significa que, técnicamente, innecesariamente, te estás abrumando con la tarea de escribir todos los detalles detallados y alucinantes necesarios para las templatedefiniciones fuera de línea . Entiendo por qué la gente quiere hacerlo, para lograr la mayor paridad con declaraciones / definiciones que no sean plantillas, para mantener la declaración de la interfaz ordenada, etc., pero no siempre vale la pena. Se trata de evaluar las compensaciones en ambos lados y elegir lo menos malo . ... hasta que se namespace classconvierta en una cosa: O [ por favor, sé una cosa ]
underscore_d
2
@ Andrew Parece haberse quedado atascado en las tuberías del Comité, aunque creo que vi a alguien decir que no fue intencional. Desearía que hubiera llegado a C ++ 17. Quizás la próxima década.
underscore_d
@CodyGray: Técnicamente, esto es lo mismo para el compilador y, por lo tanto, no reduce el tiempo de compilación. Aún así, creo que vale la pena mencionarlo y practicarlo en varios proyectos que he visto. Seguir este camino ayuda a separar la interfaz de la definición, lo cual es una buena práctica. En este caso, no ayuda con la compatibilidad ABI o similar, pero facilita la lectura y la comprensión de la interfaz.
kiloalphaindia
0

No hay nada malo con el ejemplo que ha dado. Pero debo decir que creo que no es eficiente almacenar definiciones de funciones en un archivo cpp. Solo entiendo la necesidad de separar la declaración y la definición de la función.

Cuando se usa junto con la creación de instancias de clase explícita, la Biblioteca de verificación de conceptos de Boost (BCCL) puede ayudarlo a generar código de función de plantilla en archivos cpp.

Benoît
fuente
8
¿Qué tiene de ineficiente?
Cody Gray
0

Ninguno de los anteriores funcionó para mí, así que así es como lo resolví, mi clase solo tiene 1 método.

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

esto evita errores del enlazador y no es necesario llamar a TemporaryFunction en absoluto

KronuZ
fuente