web-dev-qa-db-fra.com

Pourquoi ce modèle ne fonctionne-t-il pas comme prévu?

Je lisais sur les fonctions de modèle et je suis confus par ce problème:

#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)
}

Les résultats sont les mêmes si je n'écris pas template void g<double>(double);.

Je pense que g<double> Devrait être instancié après f(double), et donc l'appel à f dans g devrait appeler f(double). Étonnamment, il appelle toujours f(int) dans g<double>. Quelqu'un peut-il m'aider à comprendre cela?


Après avoir lu les réponses, j'ai compris ce qu'est vraiment ma confusion.

Voici un exemple mis à jour. Il est pratiquement inchangé, sauf que j'ai ajouté une spécialisation pour 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)
}

Avec la spécialisation utilisateur, g(1.0) se comporte comme je m'y attendais.

Si le compilateur ne fait pas automatiquement cette même instanciation pour g<double> Au même endroit (ou même après main(), comme décrit dans la section 26.3.3 de - Le langage de programmation C++, 4e édition)?

23
Zhongqi Cheng

Le nom f est un nom dépendant (il dépend de T via l'argument val) et il sera résolu en deux étapes :

  1. La recherche non ADL examine les déclarations de fonction ... qui sont visibles depuis le contexte de définition du modèle .
  2. ADL examine les déclarations de fonction ... qui sont visibles depuis le contexte de définition du modèle ou le contexte d'instanciation du modèle .

void f(double) n'est pas visible depuis le contexte de définition du modèle, et ADL ne le trouvera pas non plus, car

Pour les arguments de type fondamental, l'ensemble associé d'espaces de noms et de classes est vide


Nous pouvons légèrement modifier votre exemple:

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

Maintenant ADL trouvera void f(Double) dans la deuxième étape, et la sortie sera 6Double f(Double). Nous pouvons désactiver ADL en écrivant (f)(val) (Ou ::f(val)) au lieu de f(val). La sortie sera alors 6Double f(Int), en accord avec votre exemple.

12
Evg

Le problème est que f(double) n'a pas été déclarée au point où vous l'appelez; si vous déplacez sa déclaration devant le template g, il sera appelé.

Edit: Pourquoi utiliser l'instanciation manuelle?

(Je ne parlerai que des modèles de fonction, l'argumentation analogue s'applique également aux modèles de classe.) L'utilisation principale est de réduire les temps de compilation et/ou de cacher le code du modèle aux utilisateurs.

Les programmes C++ sont intégrés aux binaires en 2 étapes: compilation et liaison. Pour que la compilation d'un appel de fonction réussisse, seul l'en-tête de la fonction est nécessaire. Pour que la liaison réussisse, un fichier objet contenant le corps compilé de la fonction est nécessaire.

Désormais, lorsque le compilateur voit l'appel d'une fonction basée sur un modèle, ce qu'il fait dépend du fait qu'il connaît le corps du modèle ou uniquement l'en-tête. S'il ne voit que l'en-tête, il fait la même chose que si la fonction n'était pas basée sur un modèle: place les informations sur l'appel de l'éditeur de liens dans le fichier objet. Mais s'il voit également le corps du modèle, il fait aussi autre chose: il instancie l'instance appropriée du corps, compile ce corps et le place également dans le fichier objet.

Si plusieurs fichiers source appellent la même instance de la fonction de modèle, chacun de leurs fichiers objet contiendra une version compilée de l'instance de la fonction. (Linker le sait et résout tous les appels à une seule fonction compilée, donc il n'y en aura qu'un dans le binaire final du programme/bibliothèque.) Cependant, pour compiler chacun des fichiers source, la fonction devait être instanciée et compilé, ce qui a pris du temps.

Il suffit que l'éditeur de liens fasse son travail si le corps de la fonction se trouve dans un seul fichier objet. Instancier manuellement le modèle dans un fichier source est un moyen pour que le compilateur place le corps de la fonction dans le fichier objet du fichier source en question. (C'est un peu comme si la fonction était appelée, mais l'instanciation est écrite dans un endroit où l'appel de fonction serait invalide.) Lorsque cela est fait, tous les fichiers qui appellent votre fonction peuvent être compilés en ne connaissant que l'en-tête de la fonction, donc gain de temps qu'il faudrait pour instancier et compiler le corps de la fonction avec chacun des appels.

La deuxième raison (masquage de l'implémentation) pourrait avoir un sens maintenant. Si un auteur de bibliothèque souhaite que les utilisateurs de sa fonction de modèle puissent utiliser la fonction, il leur donne généralement le code du modèle, afin qu'ils puissent le compiler eux-mêmes. Si elle voulait garder secret le code source du modèle, elle pourrait instancier manuellement le modèle dans le code qu'elle utilise pour construire la bibliothèque et donner aux utilisateurs la version d'objet ainsi obtenue au lieu de la source.

Est-ce que cela a un sens?

6
AshleyWilkes