¿Por qué esta función de plantilla no se comporta como se esperaba?

23

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 fin gdeberí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)?

Zhongqi Cheng
fuente
3
La última llamada g(1), da i f(int)por mí. Usted escribió d f(double). ¿Era esto un error tipográfico?
HTNW
si. lo siento. actualizado
Zhongqi Cheng
El principio básico de la plantilla es admitir el uso de operaciones en los tipos de usuario, al tiempo que evita el secuestro de llamadas internas de la biblioteca por símbolos declarados por el usuario. Lo cual es un compromiso imposible, ya que no hay contratos de "concepto" para plantillas, y es demasiado tarde para introducir tales "contratos" sólidos.
curiousguy

Respuestas:

12

El nombre fes un nombre dependiente (depende Tdel argumento val) y se resolverá en dos pasos :

  1. La búsqueda sin ADL examina las declaraciones de funciones ... que son visibles desde el contexto de definición de plantilla .
  2. ADL examina las declaraciones de funciones ... que son visibles desde el contexto de definición de plantilla o el contexto de instanciación de plantilla .

void f(double)no es visible desde el contexto de definición de plantilla, y ADL tampoco lo encontrará, porque

Para argumentos de tipo fundamental, el conjunto asociado de espacios de nombres y clases está vacío


Podemos modificar ligeramente su ejemplo:

struct Int {};
struct Double : Int {};

void f(Int) { 
    std::cout << "f(Int)";
}

template<typename T>
void g(T val) {
    std::cout << typeid(val).name() << ' ';
    f(val);
    // (f)(val);
}

void f(Double) { 
    std::cout << "f(Double)";
}

int main() {
    g(Double{});
}

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 de f(val). Entonces la salida será 6Double f(Int), de acuerdo con su ejemplo.

Evg
fuente
Muchas gracias. Me pregunto dónde está la instanciación para g <double> en el código. ¿Es justo antes de main (). Si es así, ¿no debería la definición instanciada de g <double> poder ver tanto f (int) como f (double), y finalmente elegir f (double)?
Zhongqi Cheng el
@ ZhongqiCheng En el paso 1, solo se considerará el contexto de definición de plantilla , y desde ese contexto 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í.
Evg
@ ZhongqiCheng, en su edición introdujo una definición después void f(double), por lo que esta función es visible desde ella. Ahora fno es un nombre dependiente. Si hubiera una mejor coincidencia para f(val);declarada después de la definición de g<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).
Evg
Aquí está mi comprensión de su respuesta. Debo suponer que las plantillas de funciones (g <int> yg <double>) se crean instancias justo después de la definición de la plantilla. Por lo tanto, no verá f (doble). Es esto correcto. Muchas gracias.
Zhongqi Cheng el
@ ZhongqiCheng, instanciado justo antes main(). No verán f(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 no f(double).
Evg
6

El problema f(double)no se ha declarado en el punto donde lo llamas; si mueve su declaración delante del template 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?

AshleyWilkes
fuente
Le agradecería si puede explicar la diferencia entre la creación de instancias presentada en el primer código del autor y la especialización en el segundo código del autor después de la edición. He leído muchas veces el sitio de preferencias sobre especialización e instanciación y libros, pero no lo entendí. Gracias
Dev
@Dev: especifique su pregunta un poco más, no estoy seguro de qué responder. Básicamente, en este caso, la diferencia es que cuando la especialización está presente, el compilador la usa, mientras que cuando no está presente, el compilador toma la plantilla, genera una instancia de ella y usa esta instancia generada. En el código anterior, tanto la especialización como la instancia de la plantilla conducen al mismo código.
AshleyWilkes
Mi pregunta es precisamente sobre esa parte del código: "template void g <double> (double);" Se llama instanciación en la plantilla de programación, si lo sabe. La especialización es un poco diferente, ya que se declara como en el segundo código que el autor envió "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "¿Podrías explicarme la diferencia?
Dev
@Dev Ya intenté hacer eso: el compilador usa una especialización si puede; Si no puede ver la especialización (p. ej., porque no hay ninguna), el compilador crea una instancia de la plantilla y la utiliza. En el código anterior, tanto la plantilla como la especialización conducen al mismo resultado, por lo que la única diferencia está en lo que hace el compilador para llegar a ese resultado. En otros casos, la especialización podría contener cualquier implementación, no tiene que tener nada en común con la plantilla (sino para el encabezado del método). Más claro?
AshleyWilkes
1
La template void g<double>(double);llamada instanciación manual (tenga en cuenta que templatesin 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 :-)
AshleyWilkes