Estaba leyendo sobre las funciones de plantilla y me confundí con este problema:
#include <iostream>
void f(int) {
std::cout << "f(int)\n";
}
template<typename T>
void g(T val) {
std::cout << typeid(val).name() << " ";
f(val);
}
void f(double) {
std::cout << "f(double)\n";
}
template void g<double>(double);
int main() {
f(1.0); // f(double)
f(1); // f(int)
g(1.0); // d f(int), this is surprising
g(1); // i f(int)
}
Los resultados son los mismos si no escribo template void g<double>(double);
.
Creo que g<double>
debería crearse una instancia después f(double)
y, por lo tanto, la llamada a f
in g
debería llamar f(double)
. Sorprendentemente, todavía se llama f(int)
en g<double>
. ¿Alguien puede ayudarme a entender esto?
Después de leer las respuestas, descubrí cuál es realmente mi confusión.
Aquí hay un ejemplo actualizado. En su mayoría no ha cambiado, excepto que agregué una especialización para g<double>
:
#include <iostream>
void f(int){cout << "f(int)" << endl;}
template<typename T>
void g(T val)
{
cout << typeid(val).name() << " ";
f(val);
}
void f(double){cout << "f(double)" << endl;}
//Now use user specialization to replace
//template void g<double>(double);
template<>
void g<double>(double val)
{
cout << typeid(val).name() << " ";
f(val);
}
int main() {
f(1.0); // f(double)
f(1); // f(int)
g(1.0); // now d f(double)
g(1); // i f(int)
}
Con la especialización del usuario, se g(1.0)
comporta como esperaba.
¿Debería el compilador no hacer automáticamente esta misma instanciación g<double>
en el mismo lugar (o incluso después main()
, como se describe en la sección 26.3.3 de The C ++ Programming Language , cuarta edición)?
fuente
g(1)
, dai f(int)
por mí. Usted escribiód f(double)
. ¿Era esto un error tipográfico?Respuestas:
El nombre
f
es un nombre dependiente (dependeT
del argumentoval
) y se resolverá en dos pasos :void f(double)
no es visible desde el contexto de definición de plantilla, y ADL tampoco lo encontrará, porquePodemos modificar ligeramente su ejemplo:
Ahora ADL encontrará
void f(Double)
en el segundo paso, y la salida será6Double f(Double)
. Podemos desactivar ADL escribiendo(f)(val)
(o::f(val)
) en lugar def(val)
. Entonces la salida será6Double f(Int)
, de acuerdo con su ejemplo.fuente
void f(double)
no es visible: este contexto termina antes de su declaración. En el paso 2, ADL no encontrará nada, por lo que el contexto de creación de instancias de plantilla no juega ningún papel aquí.void f(double)
, por lo que esta función es visible desde ella. Ahoraf
no es un nombre dependiente. Si hubiera una mejor coincidencia paraf(val);
declarada después de la definición deg<double>
, tampoco se encontrará. La única forma de "mirar hacia adelante" es ADL (o algún compilador antiguo que no implementa la búsqueda en dos fases correctamente).main()
. No veránf(double)
, porque cuando ocurre la creación de instancias, es demasiado tarde: la fase uno de la búsqueda ya se ha realizado y se ha encontrado que nof(double)
.El problema
f(double)
no se ha declarado en el punto donde lo llamas; si mueve su declaración delante deltemplate g
, se llamará.Editar: ¿Por qué uno usaría la creación de instancias manual?
(Solo hablaré sobre plantillas de funciones, la argumentación análoga también se aplica a las plantillas de clase). El uso principal es reducir los tiempos de compilación y / o ocultar el código de la plantilla a los usuarios.
El programa C ++ está integrado en binarios en 2 pasos: compilación y vinculación. Para que la compilación de una llamada de función tenga éxito, solo se necesita el encabezado de la función. Para que la vinculación tenga éxito, se necesita un archivo de objeto que contenga el cuerpo compilado de la función.
Ahora, cuando el compilador ve una llamada de una función con plantilla , lo que hace depende de si conoce el cuerpo de la plantilla o solo el encabezado. Si solo ve el encabezado, hace lo mismo que si la función no tuviera una plantilla: coloca información sobre la llamada del vinculador al archivo de objeto. Pero si también ve el cuerpo de la plantilla, también hace otra cosa: crea una instancia adecuada del cuerpo, compila este cuerpo y lo coloca también en el archivo objeto.
Si varios archivos de origen llaman a la misma instancia de la función con plantilla, cada uno de sus archivos de objeto contendrá una versión compilada de la instancia de la función. (Linker lo sabe y resuelve todas las llamadas a una sola función compilada, por lo que solo habrá una en el binario final del programa / biblioteca). Sin embargo, para compilar cada uno de los archivos fuente, la función tuvo que ser instanciada y compilado, lo que llevó tiempo.
Es suficiente para que el enlazador haga su trabajo si el cuerpo de la función está en un archivo de objeto. Instanciar manualmente la plantilla en un archivo fuente es una forma de hacer que el compilador coloque el cuerpo de la función en el archivo objeto del archivo fuente en cuestión. (Es como si se llamara a la función, pero la instanciación se escribe en un lugar donde la llamada a la función no sería válida). Cuando se hace esto, todos los archivos que llaman a su función se pueden compilar conociendo solo el encabezado de la función, por lo tanto ahorraría tiempo para crear instancias y compilar el cuerpo de la función con cada una de las llamadas.
La segunda razón (ocultación de la implementación) podría tener sentido ahora. Si un autor de biblioteca quiere que los usuarios de su función de plantilla puedan usar la función, generalmente les da el código de la plantilla, para que puedan compilarla ellos mismos. Si quisiera mantener el código fuente de la plantilla en secreto, podría crear una instancia manual de la plantilla en el código que usa para construir la biblioteca y dar a los usuarios la versión del objeto así obtenida en lugar de la fuente.
¿Tiene esto algún sentido?
fuente
template void g<double>(double);
llamada instanciación manual (tenga en cuenta quetemplate
sin corchetes angulares, es una característica distintiva de la sintaxis); eso le dice al compilador que cree una instancia del método. Aquí tiene poco efecto, si no estuviera allí, el compilador generaría la instancia en el lugar donde se llama la instancia. La creación de instancias manual rara vez se usa, le diré por qué es posible que desee usarla después de confirmar que la cosa ahora está más clara :-)