¿Por qué las plantillas solo se pueden implementar en el archivo de encabezado?

Resuelto MainID asked hace 15 años • 19 respuestas

Cita de la biblioteca estándar de C++: un tutorial y un manual :

La única forma portátil de utilizar plantillas en este momento es implementarlas en archivos de encabezado mediante 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).

MainID avatar Jan 30 '09 17:01 MainID
Aceptado

Advertencia: no es necesario colocar 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 que su código falla 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é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 crear una instancia de ellos 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 para esto es escribir la declaración de 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 el compilador puede acceder a ella.

Solución alternativa

Otra solución es mantener la implementación separada y crear instancias explícitas de 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 .

Luc Touraille avatar Jan 30 '2009 10:01 Luc Touraille

Esto se debe al requisito de una compilación separada y a que las plantillas son polimorfismos de estilo de creación de instancias.

Acerquémonos un poco más al concreto para obtener una explicación. Digamos que tengo los siguientes archivos:

  • foo.h
    • declara la interfaz declass MyClass<T>
  • foo.cpp
    • define la implementación declass MyClass<T>
  • barra.cpp
    • usosMyClass<int>

La compilación separada significa que debería poder compilar foo.cpp independientemente de bar.cpp . El compilador realiza 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 de todo el programa. Sólo 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 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 volver a compilar foo. .cpp . foo.cpp podría incluso 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 yo escribiera foo.cpp .

"Polimorfismo de estilo de creación de instancias" significa que la plantilla MyClass<T>no es realmente una clase genérica que pueda compilarse en un código que pueda funcionar para cualquier valor de T. Eso agregaría gastos generales, 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 casi idénticos class MyClass_int, class MyClass_floatetc., pero aún así poder terminar con un código compilado que sea principalmente como si hubiéramos escrito cada versión por separado. Entonces una plantilla es literalmente una plantilla; una plantilla de clase no es una clase, es una receta para crear una nueva clase para cada una de Tlas que encontramos. Una plantilla no se puede compilar en 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 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 se compila bar.cpp , 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 crearla.

Si foo.cpp usa MyClass<int>, entonces el código para eso se generará mientras se compila foo.cpp , de modo que cuando bar.o esté vinculado a foo.o, se podrán conectar 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 única plantilla. Pero no hay forma de que bar.cpp use la plantilla como plantilla y cree una instancia de ella en cualquier tipo que desee; sólo puede utilizar versiones preexistentes de la clase con plantilla que el autor de foo.cpp pensó proporcionar.

Se podría pensar que al compilar una plantilla el compilador debería "generar todas las versiones", y las que nunca se utilizan se filtran durante la vinculación. Aparte de los enormes gastos generales y las dificultades extremas que enfrentaría tal enfoque porque las características del "modificador de tipo" como punteros y matrices permiten que incluso los tipos integrados den lugar a un número infinito de tipos, ¿qué sucede cuando ahora extiendo mi programa? añadiendo:

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

No hay manera posible de que esto funcione a menos que

  1. Tenemos que recompilar foo.cpp cada vez que cambiemos cualquier otro archivo en el programa , en caso de que agregue una nueva instancia novedosa deMyClass<T>
  2. Requerir que baz.cpp contenga (posiblemente a través del encabezado include) la plantilla completa de MyClass<T>, para que el compilador pueda generarla MyClass<BazPrivate>durante la compilación de baz.cpp .

A nadie le gusta (1), porque los sistemas de compilación de análisis de programas completos tardan una eternidad en compilarse y porque hace imposible distribuir bibliotecas compiladas sin el código fuente. Entonces tenemos (2) en su lugar.

Ben avatar May 11 '2013 03:05 Ben

Hay muchas respuestas correctas aquí, pero quería agregar esto (para que esté completo):

Si usted, en la parte inferior del archivo cpp de implementación, crea instancias explícitas de todos los tipos con los que se utilizará la plantilla, el vinculador podrá encontrarlos como de costumbre.

Editar: Agregar un ejemplo de creación de instancias de plantilla explícita. Se utiliza después de que se haya definido la plantilla y se hayan definido todas las funciones miembro.

template class vector<int>;

Esto creará una instancia (y por lo tanto pondrá a disposición del vinculador) la clase y todas sus funciones miembro (únicamente). Una sintaxis similar funciona para las plantillas de funciones, por lo que si tiene sobrecargas de operadores que no son miembros, es posible que deba hacer lo mismo con ellas.

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?) lo usa extern template class vector<int>para evitar que se cree una instancia en todos los demás (¿1000?) archivos que usan vector.

MaHuJa avatar Aug 13 '2009 13:08 MaHuJa

El compilador debe crear una instancia de las plantillas antes de compilarlas en código objeto. Esta creación de instancias sólo se puede lograr si se conocen los argumentos de la plantilla. Ahora imagine un escenario en el que una función de plantilla se declara a.h, se define a.cppy se utiliza en b.cpp. Cuando a.cppse compila, no se sabe necesariamente que la próxima compilación b.cpprequerirá una instancia de la plantilla, y mucho menos qué instancia específica sería esa. Para más archivos de encabezado y fuente, la situación puede complicarse rápidamente.

Se puede argumentar que los compiladores pueden hacerse 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. AFAIK, los compiladores no miran hacia el futuro. 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?).

David Hanak avatar Jan 30 '2009 10:01 David Hanak