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

23

Je lisais sur les fonctions de modèle et je suis devenu 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 g<double>à instancier après f(double), et donc l'appel à fen gdevrait appeler f(double). Étonnamment, il appelle encore f(int)à 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.

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

Zhongqi Cheng
la source
3
Le dernier appel g(1),, i f(int)me donne . Vous avez écrit d f(double). C'était une faute de frappe?
HTNW
Oui. Désolé. mise à jour
Zhongqi Cheng
Le principe de base du modèle est de prendre en charge l'utilisation d'opérations sur les types d'utilisateurs, tout en empêchant le détournement d'appels de bibliothèque interne par des symboles déclarés par l'utilisateur. Ce qui est un compromis impossible, car il n'y a pas de contrats «conceptuels» pour les modèles, et il est trop tard pour introduire de tels «contrats» solides.
curiousguy

Réponses:

12

Le nom fest un nom dépendant (cela dépend Tvia 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 fonctions ... qui sont visibles à partir du contexte de définition de modèle ou du contexte d'instanciation de modèle .

void f(double)n'est pas visible dans le contexte de définition de 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). Ensuite, la sortie sera 6Double f(Int), en accord avec votre exemple.

Evg
la source
Merci beaucoup. Je me demande où l'instanciation de g <double> est dans le code. Est-ce juste avant main (). Dans l'affirmative, la définition instanciée de g <double> ne devrait-elle pas être capable de voir à la fois f (int) et f (double), et finalement choisir f (double)?
Zhongqi Cheng
@ZhongqiCheng À l'étape 1, seul le contexte de définition de modèle sera pris en compte, et à partir de ce contexte void f(double)n'est pas visible - ce contexte se termine avant sa déclaration. À l'étape 2, ADL ne trouvera rien, donc le contexte d' instanciation du modèle ne joue aucun rôle ici.
Evg
@ZhongqiCheng, dans votre édition, vous avez introduit une définition après void f(double), donc cette fonction est visible d'elle. Ce fn'est pas un nom dépendant. S'il y avait une meilleure correspondance pour f(val);déclaré après la définition de g<double>, elle ne sera pas trouvée non plus. La seule façon de «regarder vers l'avenir» est ADL (ou un ancien compilateur qui n'implémente pas correctement la recherche en deux phases).
Evg
Voici ma compréhension de votre réponse. Je devrais supposer que les modèles de fonction (g <int> et g <double>) sont instanciés juste après la définition du modèle. Par conséquent, il ne verra pas f (double). Est-ce correct. Merci beaucoup.
Zhongqi Cheng
@ZhongqiCheng, instancié juste avant main(). Ils ne verront pas f(double), car lorsque l'instanciation se produit, il est trop tard: la première phase de la recherche a déjà été effectuée et n'a trouvé aucun résultat f(double).
Evg
6

Le problème f(double)n'a pas été déclaré 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 de s'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 de faire en sorte 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 (la dissimulation 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?

AshleyWilkes
la source
Je vous serais reconnaissant de bien vouloir expliquer la différence entre l'instanciation présentée dans le premier code de l'auteur, et la spécialisation dans le second code de l'auteur après édition. J'ai lu plusieurs fois le site de cppreference sur la spécialisation et l'instanciation et les livres, mais je n'ai pas compris. Merci
Dev
@Dev: Veuillez préciser un peu plus votre question, je ne sais pas trop quoi répondre. Fondamentalement, dans ce cas, la différence est que lorsque la spécialisation est présente, le compilateur l'utilise, tandis que lorsqu'elle n'est pas présente, le compilateur prend le modèle, en génère une instance et utilise cette instance générée. Dans le code ci-dessus, la spécialisation et l'instance du modèle conduisent au même code.
AshleyWilkes
Ma question porte précisément sur cette partie du code: "template void g <double> (double);" Il est nommé instanciation dans le modèle de programmation, si vous le savez. La spécialisation est un peu différente, car elle est déclarée comme dans le deuxième code, l'auteur a envoyé "template <> void g <double> (double val) {cout << typeid (val) .name () <<" "; f ( val);} "Pourriez-vous m'expliquer la différence?
Dev
@Dev J'ai déjà essayé de le faire: le compilateur utilise une spécialisation s'il le peut; s'il ne peut pas voir la spécialisation (par exemple parce qu'il n'y en a pas), le compilateur crée une instance du modèle et utilise cette instance. Dans le code ci-dessus, le modèle et la spécialisation conduisent au même résultat, donc la seule différence est dans ce que fait le compilateur pour arriver à ce résultat. Dans d'autres cas, la spécialisation peut contenir n'importe quelle implémentation, elle n'a rien à avoir en commun avec le modèle (mais pour l'en-tête de la méthode). Plus clair?
AshleyWilkes
1
La template void g<double>(double);soi-disant instanciation manuelle (notez le templatesans parenthèses angulaires, c'est une caractéristique distinctive de la syntaxe); qui indique au compilateur de créer une instance de la méthode. Ici, cela a peu d'effet, s'il n'était pas là, le compilateur générerait l'instance à l'endroit où l'instance est appelée. L'instanciation manuelle est une fonctionnalité rarement utilisée, je dirai pourquoi vous voudrez peut-être l'utiliser après avoir confirmé que la chose est maintenant plus claire :-)
AshleyWilkes