Por que essa função de modelo não se comporta conforme o esperado?

23

Eu estava lendo sobre funções de modelo e fiquei confuso com 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)
}

Os resultados são os mesmos se eu não escrever template void g<double>(double);.

Eu acho que g<double>deveria ser instanciado depois f(double)e, portanto, a chamada para fin gdeveria chamar f(double). Surpreendentemente, ele ainda chama f(int)no g<double>. Alguém pode me ajudar a entender isso?


Depois de ler as respostas, descobri qual é realmente a minha confusão.

Aqui está um exemplo atualizado. É praticamente inalterado, exceto que eu adicionei uma especialização 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)
}

Com a especialização do usuário, g(1.0)comporta-se como eu esperava.

O compilador não deve fazer automaticamente essa mesma instanciação g<double>no mesmo local (ou mesmo depois main(), conforme descrito na seção 26.3.3 da Linguagem de programação C ++ , 4ª edição)?

Zhongqi Cheng
fonte
3
A última ligação g(1), dá i f(int)para mim. Você escreveu d f(double). Isso foi um erro de digitação?
HTNW 29/11/19
sim. desculpa. atualizado
Zhongqi Cheng 29/11
O princípio básico do modelo é oferecer suporte ao uso de operações em tipos de usuário, enquanto ainda impede o seqüestro de chamadas de biblioteca interna por símbolos declarados pelo usuário. O que é um compromisso impossível, pois não há contratos de "conceito" para modelos e é tarde demais para introduzir esses "contratos" sólidos.
curiousguy

Respostas:

12

O nome fé um nome dependente (depende Tdo argumento val) e será resolvido em duas etapas :

  1. A pesquisa não ADL examina declarações de função ... que são visíveis no contexto de definição do modelo .
  2. A ADL examina declarações de função ... visíveis no contexto de definição do modelo ou no contexto de instanciação do modelo .

void f(double)não é visível no contexto de definição do modelo e a ADL também não o encontrará, porque

Para argumentos do tipo fundamental, o conjunto associado de namespaces e classes está vazio


Podemos modificar um pouco o seu exemplo:

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{});
}

Agora, o ADL encontrará void f(Double)na segunda etapa e a saída será 6Double f(Double). Podemos desativar a ADL escrevendo (f)(val)(ou ::f(val)) em vez de f(val). Então a saída será 6Double f(Int), de acordo com o seu exemplo.

Evg
fonte
Muito obrigado. Eu estou querendo saber onde a instanciação para g <double> está no código. É logo antes de main (). Nesse caso, a definição instanciada g <double> não deveria ser capaz de ver f (int) ef (double) e finalmente escolher f (double)?
Zhongqi Cheng 29/11/19
@ZhongqiCheng Na etapa 1, apenas o contexto de definição do modelo será considerado e a partir desse contexto void f(double)não é visível - esse contexto termina antes de sua declaração. Na etapa 2, o ADL não encontrará nada; portanto, o contexto de instanciação do modelo não desempenha nenhum papel aqui.
Evg
@ZhongqiCheng, em sua edição, você introduziu uma definição depois void f(double), então essa função é visível a partir dela. Agora fnão é um nome dependente. Se houver uma correspondência melhor para f(val);declarada após a definição de g<double>, ela também não será encontrada. A única maneira de "olhar adiante" é o ADL (ou algum compilador antigo que não implementa a pesquisa em duas fases corretamente).
Evg
Aqui está o meu entendimento da sua resposta. Devo assumir que os modelos de função (g <int> eg <double>) são instanciados logo após a definição do modelo. Portanto, ele não verá f (duplo). Isso está correto? Muito obrigado.
Zhongqi Cheng 30/11/19
@ZhongqiCheng, instanciado logo antes main(). Eles não verão f(double), porque quando a instanciação acontece, é tarde demais: a primeira fase da pesquisa já foi realizada e não foi encontrada f(double).
Evg
6

O problema é f(double)que não foi declarado no ponto em que você o chama; se você mover sua declaração para frente template g, ela será chamada.

Edit: Por que alguém usaria instanciação manual?

(Vou falar apenas sobre modelos de função, também há argumentos análogos para modelos de classe.) O uso principal é reduzir o tempo de compilação e / ou ocultar o código do modelo dos usuários.

O programa C ++ é incorporado em binários em 2 etapas: compilação e vinculação. Para a compilação de uma chamada de função ter sucesso, apenas o cabeçalho da função é necessário. Para que a vinculação seja bem-sucedida, é necessário um arquivo de objeto contendo o corpo compilado da função.

Agora, quando o compilador vê uma chamada de uma função de modelo , o que ele faz depende se conhece o corpo do modelo ou apenas o cabeçalho. Se ele vir apenas o cabeçalho, fará o mesmo que se a função não estivesse modelada: coloca informações sobre a chamada do vinculador para o arquivo de objeto. Mas se ele também vê o corpo do modelo, ele também faz outra coisa: instancia a instância apropriada do corpo, compila esse corpo e o coloca no arquivo de objeto também.

Se vários arquivos de origem chamarem a mesma instância da função de modelo, cada um de seus arquivos de objeto conterá uma versão compilada da instância da função. (O Linker sabe disso e resolve todas as chamadas para uma única função compilada, portanto, haverá apenas uma no binário final do programa / biblioteca.) No entanto, para compilar cada um dos arquivos de origem, a função teve que ser instanciada e compilado, o que levou tempo.

É suficiente para o vinculador fazer seu trabalho se o corpo da função estiver em um arquivo de objeto. Instanciar manualmente o modelo em um arquivo de origem é uma maneira de fazer o compilador colocar o corpo da função no arquivo de objeto do arquivo de origem em questão. (É como se a função fosse chamada, mas a instanciação é escrita em um local onde a chamada da função seria inválida.) Quando isso é feito, todos os arquivos que chamam a sua função podem ser compilados sabendo apenas o cabeçalho da função, portanto economizando tempo para instanciar e compilar o corpo da função com cada uma das chamadas.

A segunda razão (ocultação da implementação) pode fazer sentido agora. Se um autor da biblioteca deseja que os usuários de sua função de modelo possam usar a função, ela geralmente fornece o código do modelo, para que eles mesmos possam compilá-lo. Se ela quisesse manter em segredo o código fonte do modelo, ela poderia instanciar manualmente o modelo no código que ela usa para criar a biblioteca e fornecer aos usuários a versão do objeto assim obtida em vez da fonte.

Isto faz algum sentido?

AshleyWilkes
fonte
Ficarei grato se você puder explicar a diferença entre a instanciação apresentada no primeiro código do autor e a especialização no segundo código do autor após a edição. Li muitas vezes o site da cppreference sobre especialização, instanciação e livros, mas não entendi. Obrigado
Dev
@ Dev: Por favor, especifique um pouco mais a sua pergunta, não sei ao certo o que responder. Basicamente, nesse caso, a diferença é que, quando a especialização está presente, o compilador a usa, enquanto quando não está presente, o compilador pega o modelo, gera uma instância e usa essa instância gerada. No código acima, a especialização e a instância do modelo levam ao mesmo código.
AshleyWilkes
Minha pergunta é justamente sobre essa parte do código: "template void g <double> (double);" É chamado de instanciação no modelo de programação, se você souber disso. A especialização é um pouco diferente, pois é declarado como no segundo código que o autor enviou "template <> void g <double> (duplo valor) {cout << typeid (val) .name () <<" "; f ( val);} "Você poderia me explicar a diferença?
Dev
@ Dev Eu já tentei fazer isso: o compilador usa uma especialização, se puder; se não conseguir ver a especialização (por exemplo, porque não há nenhuma), o compilador cria uma instância do modelo e a usa. No código acima, o modelo e a especialização levam ao mesmo resultado; portanto, a única diferença está no que o compilador faz para obter esse resultado. Em outros casos, a especialização pode conter qualquer implementação, não precisa ter nada em comum com o modelo (exceto no cabeçalho do método). Mais claro?
AshleyWilkes
11
A template void g<double>(double);chamada instanciação manual (observe a opção templatesem colchetes angulares, esse é um recurso distintivo da sintaxe); que diz ao compilador para criar uma instância do método Aqui, ele tem pouco efeito; se não estivesse lá, o compilador geraria a instância no local em que a instância é chamada. A instanciação manual raramente é usada, vou dizer por que você pode querer usá-la depois de confirmar que a coisa está mais clara agora :-) #
55640