Pregunta ¿Por qué las plantillas solo pueden implementarse en el archivo de encabezado?


Cita de La biblioteca estándar de C ++: un tutorial y 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 solamente solución portátil. Pero son la solución portátil más conveniente).


1383
2018-01-30 10:06


origen


Respuestas:


Es no necesario para 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 cual su código está fallando es que, al crear una instancia de 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émoslo 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 instanciar la plantilla.

Una solución común a esto es escribir la declaración de plantilla en un archivo de cabecera, 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 echarle un vistazo al C ++ Super-Preguntas frecuentes sobre este tema.


1206
2018-01-30 10:26



Aquí hay muchas respuestas correctas, 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 de la forma habitual.

Editar: Agregar ejemplo de instanciación de plantilla explícita. Se usa después de que se haya definido la plantilla y se hayan definido todas las funciones de los miembros.

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 para las funciones de plantilla, por lo que si tiene sobrecargas de operador no miembro puede que tenga que hacer lo mismo.

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?) Utiliza extern template class vector<int> a fin de evitar que la instancia de todo en el otro (1000?) Archivos que usan vectores.


200
2017-08-13 13:49



Es por el requerimiento de compilación separada y porque las plantillas son polimorfismo de instanciación.

Vamos a acercarnos un poco más al concreto para obtener 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 ser capaz de compilar foo.cpp independientemente de bar.cpp. El compilador hace todo el trabajo 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 de programa completo. Solo el enlazador 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 todavía debería ser capaz de vincular el foo.o Ya tuve 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, distribuida en otro lugar sin foo.cpp, y vinculado con el código que escriben años después de que escribí foo.cpp.

"Polimorfismo de tipo instanciación" significa que la plantilla MyClass<T> no es realmente una clase genérica que se puede compilar a código que puede funcionar para cualquier valor de T. Eso agregaría sobrecarga, como el boxeo, la necesidad de pasar indicadores de función a asignadores y constructores, etc. La intención de las plantillas C ++ es evitar tener que escribir casi idéntico class MyClass_int, class MyClass_float, etc., pero para poder terminar con un código compilado que es casi como si tuviéramos tenido escrito cada versión por separado. Entonces una plantilla es literalmente una plantilla; una plantilla de clase es no una clase, es una receta para crear una nueva clase para cada T Nos encontramos. Una plantilla no se puede compilar en el código, solo se puede compilar el resultado de instanciar la plantilla.

Así que cuando foo.cpp está compilado, el compilador no puede ver bar.cpp para saberlo 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 bar.cpp se compila, el compilador puede ver que necesita crear un MyClass<int>, pero no puede ver la plantilla MyClass<T> (solo su interfaz en foo.h) por lo que no puede crearlo.

Si foo.cpp ella misma usa MyClass<int>, entonces se generará código para eso mientras compila foo.cpp, así que cuando bar.o está vinculado a foo.o se pueden conectar y funcionarán. Podemos usar ese hecho para permitir que un conjunto finito de instancias de plantillas se implemente en un archivo .cpp escribiendo una sola plantilla. Pero no hay forma de bar.cpp para usar la plantilla como plantilla y ejemplificarlo en cualquier tipo que le guste; solo puede usar versiones preexistentes de la clase con plantilla que el autor de foo.cpp pensado para proporcionar.

Puede pensar que al compilar una plantilla, el compilador debe "generar todas las versiones", y las que nunca se utilizan se filtran durante la vinculación. Además de los enormes gastos generales y las dificultades extremas que enfrentaría un enfoque de este tipo porque las características de "modificador de tipo" como punteros y matrices permiten incluso que los tipos incorporados generen un número infinito de tipos, ¿qué ocurre cuando extiendo mi programa? añadiendo:

  • baz.cpp
    • declara e implementa class BazPrivatey usa MyClass<BazPrivate>

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

  1. Tengo que recompilar foo.cpp cada vez que cambiamos cualquier otro archivo en el programa, en caso de que agregara una nueva instanciación nueva de MyClass<T>
  2. Exigir que baz.cpp contiene (posiblemente a través de 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 toman Siempre para compilar, y porque hace que sea imposible distribuir bibliotecas compiladas sin el código fuente. Entonces tenemos (2) en su lugar.


173
2018-05-11 03:54



Las plantillas deben ser instanciado por el compilador antes de realmente compilarlos en código de objeto. Esta creación de instancias solo se puede lograr si los argumentos de la plantilla son conocidos. Ahora imagine un escenario donde se declara una función de plantilla en a.h, definido en a.cpp y utilizado en b.cpp. Cuando a.cpp está compilado, no se sabe necesariamente que la próxima compilación b.cpp requerirá una instancia de la plantilla, sin mencionar qué instancia específica sería esa. Para obtener más archivos de encabezado y de origen, la situación puede volverse más complicada rápidamente.

Se puede argumentar que los compiladores pueden ser más inteligentes para "mirar hacia el futuro" para todos los usos de la plantilla, pero estoy seguro de que no sería difícil crear escenarios recursivos o complicados de otra manera. AFAIK, los compiladores no hacen tales look aheads. Como señaló Anton, algunos compiladores admiten declaraciones de exportación explícitas de instancias de plantillas, pero no todos los compiladores lo soportan (¿todavía?).


64
2018-01-30 10:23



En realidad, las versiones del estándar C ++ antes de C ++ 11 definían la palabra clave 'export', que haría Permitir simplemente declarar plantillas en un archivo de cabecera e implementarlas en otro lugar.

Desafortunadamente, ninguno de los compiladores populares implementó esta palabra clave. El único que conozco es el frontend escrito por Edison Design Group, que es utilizado por el compilador Comeau C ++. Todos los demás insistieron en que escribieras plantillas en los archivos de encabezado, necesitando la definición del código para la creación de instancias adecuada (como ya se señaló).

Como resultado, el comité estándar de ISO C ++ decidió eliminar el export característica de las plantillas que comienzan con C ++ 11.


47
2018-01-30 13:38



Aunque C ++ estándar no tiene tal requerimiento, algunos compiladores requieren que todas las plantillas de funciones y clases 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 los compiladores no permitirán que se definan en archivos sin encabezado, como archivos .cpp

Hay un exportar palabra clave que se supone que mitiga este problema, pero no está ni cerca de ser portátil.


31
2018-01-30 10:15



Las plantillas se deben usar en los encabezados porque el compilador necesita crear instancias de diferentes versiones del código, según 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 una plantilla en un .cpparchivo, está compilando una función / clase concreta. Este no es el caso para las plantillas, que pueden crearse instancias con diferentes tipos, es decir, se debe emitir un código concreto cuando se reemplazan los parámetros de la plantilla por tipos concretos.

Había una función con el export palabra clave que debía usarse para compilación separada. los export la característica está en desuso en C++11 y, AFAIK, solo un compilador lo implementó. No deberías hacer uso de export. La compilación separada no es posible en C++ o C++11 pero tal vez en C++17, si los conceptos entran, podríamos tener alguna forma de compilación por separado.

Para lograr una compilación separada, debe ser posible la verificación del cuerpo de la plantilla por separado. Parece que una solución es posible con conceptos. Mira esto papel presentado recientemente en el reunión del comité de normas. Creo que este no es el único requisito, ya que aún necesita crear una instancia del código para la plantilla en el código de usuario.

El problema de compilación por separado para las plantillas Supongo que también es un problema que está surgiendo con la migración a los módulos, que actualmente se está trabajando.


26
2018-05-12 16:42