Pourquoi C ++ ne peut-il pas déduire T dans un appel à Foo <T> :: Foo (T &&)?

9

Étant donné la structure de modèle suivante:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

Cela compile et Tse déduit comme int:

auto f = Foo(2);

Mais cela ne compile pas: https://godbolt.org/z/hAA9TE

int x = 2;
auto f = Foo(x);

/*
<source>:12:15: error: no viable constructor or deduction guide for deduction of template arguments of 'Foo'
    auto f = Foo(x);
             ^

<source>:7:5: note: candidate function [with T = int] not viable: no known conversion from 'int' to 'int &&' for 1st argument
    Foo(T&&) {}
    ^
*/

Cependant, Foo<int&>(x)est accepté.

Mais lorsque j'ajoute un guide de déduction défini par l'utilisateur apparemment redondant, cela fonctionne:

template<typename T>
Foo(T&&) -> Foo<T>;

Pourquoi ne peut-on pas Têtre déduit int&sans un guide de déduction défini par l'utilisateur?

jtbandes
la source
Cette question semble concerner les types de modèles tels queFoo<T<A>>
jtbandes

Réponses:

5

Je pense que la confusion survient ici car il existe une exception spécifique pour les guides de déduction synthétisés concernant les références de transfert.

Il est vrai que la fonction candidate aux fins de la déduction d'arguments de modèle de classe générée à partir du constructeur et celle générée à partir du guide de déduction défini par l'utilisateur sont exactement les mêmes, c'est-à-dire:

template<typename T>
auto f(T&&) -> Foo<T>;

mais pour celui généré par le constructeur, il T&&s'agit d'une simple référence rvalue, alors qu'il s'agit d'une référence de transfert dans le cas défini par l'utilisateur. Ceci est spécifié par [temp.deduct.call] / 3 de la norme C ++ 17 (projet N4659, mettez l'accent sur le mien):

Une référence de transfert est une référence rvalue à un paramètre de modèle non qualifié cv qui ne représente pas un paramètre de modèle d'un modèle de classe (pendant la déduction d'argument de modèle de classe ([over.match.class.deduct])).

Par conséquent, le candidat synthétisé à partir du constructeur de classe ne déduira pas Tcomme s'il Tprovenait d'une référence de transfert (qui pourrait en déduire être une référence lvalue, de sorte qu'il T&&s'agit également d'une référence lvalue), mais au lieu de cela, il ne déduira Tque comme non-référence, de sorte que T&&c'est toujours une référence rvalue.

noyer
la source
1
Merci pour la réponse claire et concise. Savez-vous pourquoi il existe une exception pour les paramètres de modèle de classe dans les règles de transfert des références?
jtbandes
2
@jtbandes Cela semble avoir été changé à la suite d'un commentaire de l'organisme national américain, voir l'article p0512r0 . Je n'ai pas pu trouver le commentaire cependant. Je suppose que la justification est que si vous écrivez un constructeur prenant une référence rvalue, vous vous attendez généralement à ce qu'il fonctionne de la même manière, que vous spécifiez un Foo<int>(...)ou juste Foo(...), ce qui n'est pas le cas avec les références de transfert (qui pourraient en déduire à la Foo<int&>place.
Walnut
6

Le problème ici est que, puisque la classe est basée sur un modèle T, dans le constructeur, Foo(T&&)nous n'effectuons pas de déduction de type; Nous avons toujours une référence de valeur r. Autrement dit, le constructeur de Fooressemble en fait à ceci:

Foo(int&&)

Foo(2)fonctionne parce que 2c'est une valeur.

Foo(x)ne le fait pas car il xs'agit d'une valeur l qui ne peut pas être liée int&&. Vous pouvez faire std::move(x)pour le caster dans le type approprié ( démo )

Foo<int&>(x)fonctionne très bien car le constructeur devient Foo(int&)dû à des règles d'effondrement de référence; au départ, c'est ce Foo((int&)&&)qui s'effondre Foo(int&)selon la norme.

En ce qui concerne votre guide de déduction "redondant": Au départ, il existe un guide de déduction de modèle par défaut pour le code qui agit essentiellement comme une fonction d'aide comme ceci:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

template<typename T>
Foo<T> MakeFoo(std::add_rvalue_reference_t<T> value)
{
   return Foo<T>(std::move(value));
}

//... 
auto f = MakeFoo(x);

Cela est dû au fait que la norme dicte que cette méthode de modèle (fictive) a les mêmes paramètres de modèle que la classe (Just T), suivis de tout paramètre de modèle comme le constructeur (aucun dans ce cas; le constructeur n'est pas basé sur des modèles). Ensuite, les types des paramètres de fonction sont les mêmes que ceux du constructeur. Dans notre cas, après instanciation Foo<int>, le constructeur ressemble à Foo(int&&)une rvalue-reference en d'autres termes. D'où l'utilisation de ce qui add_rvalue_reference_tprécède.

Évidemment, cela ne fonctionne pas.

Lorsque vous avez ajouté votre guide de déduction «redondant»:

template<typename T>
Foo(T&&) -> Foo<T>;

Vous avez permis au compilateur de distinguer que, malgré toute référence attachée à Tdans le constructeur ( int&, const int&ou int&&etc.), vous avez voulu le type déduit de la classe sans référence (juste T). En effet , nous soudainement nous accomplissons l' inférence de type.

Maintenant, nous générons une autre fonction d'aide (fictive) qui ressemble à ceci:

template<class U>
Foo<U> MakeFoo(U&& u)
{
   return Foo<U>(std::forward<U>(u));
}

// ...
auto f = MakeFoo(x);

(Nos appels au constructeur sont redirigés vers la fonction d'assistance aux fins de déduction des arguments du modèle de classe, Foo(x)devient ainsi MakeFoo(x)).

Cela permet U&&de devenir int&et Tde devenir simplementint

AndyG
la source
Le deuxième niveau de modèle ne semble pas nécessaire; quelle valeur apporte-t-il? Pouvez-vous fournir un lien vers une documentation qui explique pourquoi T&& est toujours traité comme une référence de valeur ici?
jtbandes
1
Mais si T n'a pas encore été déduit, que signifie "tout type convertible en T"?
jtbandes
1
Vous êtes rapide pour proposer une solution de contournement, mais pouvez-vous vous concentrer davantage sur l'explication de la raison pour laquelle cela ne fonctionne pas, sauf si vous le modifiez d'une manière ou d'une autre? Pourquoi ça ne marche pas tel quel? " xest une valeur qui ne peut pas se lier à int&&" mais quelqu'un qui ne comprend pas sera perplexe qui Foo<int&>(x)pourrait fonctionner mais n'a pas été compris automatiquement - je pense que nous voulons tous une compréhension plus profonde de pourquoi.
Wyck
2
@Wyck: J'ai mis à jour le message pour me concentrer davantage sur le pourquoi.
AndyG
3
@Wyck La vraie raison est très simple: la norme a spécifiquement désactivé la sémantique de transfert parfaite dans les guides de déduction synthétisés pour éviter les surprises.
LF