Comment fonctionne `void_t`

149

J'ai regardé le discours de Walter Brown au Cppcon14 sur la programmation de modèles modernes ( Partie I , Partie II ) où il a présenté sa void_ttechnique SFINAE.

Exemple: étant
donné un modèle de variable simple qui évalue voidsi tous les arguments du modèle sont bien formés:

template< class ... > using void_t = void;

et le trait suivant qui vérifie l'existence d'une variable membre appelée membre :

template< class , class = void >
struct has_member : std::false_type
{ };

// specialized as has_member< T , void > or discarded (sfinae)
template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : std::true_type
{ };

J'ai essayé de comprendre pourquoi et comment cela fonctionne. Par conséquent, un petit exemple:

class A {
public:
    int member;
};

class B {
};

static_assert( has_member< A >::value , "A" );
static_assert( has_member< B >::value , "B" );

1. has_member< A >

  • has_member< A , void_t< decltype( A::member ) > >
    • A::member existe
    • decltype( A::member ) est bien formé
    • void_t<> est valide et évalue à void
  • has_member< A , void > et donc il choisit le modèle spécialisé
  • has_member< T , void > et évalue à true_type

2. has_member< B >

  • has_member< B , void_t< decltype( B::member ) > >
    • B::member n'existe pas
    • decltype( B::member ) est mal formé et échoue silencieusement (sfinae)
    • has_member< B , expression-sfinae > donc ce modèle est supprimé
  • le compilateur trouve has_member< B , class = void >avec void comme argument par défaut
  • has_member< B > évalue à false_type

http://ideone.com/HCTlBb

Questions:
1. Ma compréhension de cela est-elle correcte?
2. Walter Brown déclare que l'argument par défaut doit être exactement du même type que celui utilisé void_tpour que cela fonctionne. Pourquoi donc? (Je ne vois pas pourquoi ces types doivent correspondre, est-ce que n'importe quel type par défaut fait le travail?)

non-compensation
la source
6
Ad 2) Imaginez l'assert statique a été écrit: has_member<A,int>::value. Ensuite, la spécialisation partielle évaluée has_member<A,void>ne peut pas correspondre. Par conséquent, il doit être has_member<A,void>::value, ou, avec le sucre syntaxique, un argument par défaut de type void.
dyp
1
@dyp Merci, je vais éditer ça. Mh, je ne vois pas encore la nécessité d'avoir un has_member< T , class = void >défaut void. En supposant que ce trait ne sera utilisé qu'avec 1 argument de modèle à tout moment, alors l'argument par défaut pourrait être n'importe quel type?
compensation
Question interessante.
AStopher du
2
Notez que dans cette proposition, open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4436.pdf , Walter a changé template <class, class = void>pour template <class, class = void_t<>>. Alors maintenant, nous sommes libres de faire ce que nous voulons avec l' void_timplémentation du modèle d'alias :)
JohnKoch

Réponses:

133

1. Modèle de classe primaire

Lorsque vous écrivez has_member<A>::value, le compilateur recherche le nom has_memberet trouve le modèle de classe primaire , c'est-à-dire cette déclaration:

template< class , class = void >
struct has_member;

(Dans l'OP, c'est écrit comme une définition.)

La liste des arguments de modèle <A>est comparée à la liste de paramètres de modèle de ce modèle principal. Étant donné que le modèle primaire a deux paramètres, mais vous ne fourni un, le paramètre reste est réglé par défaut à l'argument de modèle par défaut: void. C'est comme si tu avais écrit has_member<A, void>::value.

2. Modèle de classe spécialisée

Désormais , la liste des paramètres du modèle est comparée à toutes les spécialisations du modèle has_member. Seulement si aucune spécialisation ne correspond, la définition du modèle principal est utilisée comme solution de secours. Ainsi, la spécialisation partielle est prise en compte:

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

Le compilateur essaie de faire correspondre les arguments du modèle A, voidavec les modèles définis dans la spécialisation partielle: Tet void_t<..>un par un. Tout d'abord , la déduction des arguments de modèle est effectuée. La spécialisation partielle ci-dessus est toujours un modèle avec des paramètres de modèle qui doivent être "remplis" par des arguments.

Le premier modèle T , permet au compilateur de déduire le paramètre-modèle T. C'est une déduction triviale, mais considérez un modèle comme T const&, où nous pourrions encore déduire T. Pour le modèle Tet l'argument modèle A, nous en déduisons Têtre A.

Dans le deuxième modèle void_t< decltype( T::member ) > , le paramètre de modèle Tapparaît dans un contexte où il ne peut être déduit d'aucun argument de modèle.

Il y a deux raisons à cela:

  • L'expression à l'intérieur decltypeest explicitement exclue de la déduction des arguments de modèle. Je suppose que c'est parce que cela peut être arbitrairement complexe.

  • Même si nous avons utilisé un modèle sans decltypelike void_t< T >, alors la déduction de Tse produit sur le modèle d'alias résolu. Autrement dit, nous résolvons le modèle d'alias et essayons plus tard de déduire le type Tdu modèle résultant. Le modèle résultant, cependant, est void, qui ne dépend pas de Tet ne nous permet donc pas de trouver un type spécifique pour T. Ceci est similaire au problème mathématique consistant à essayer d'inverser une fonction constante (au sens mathématique de ces termes).

La déduction des arguments de modèle est terminée (*) , maintenant les arguments de modèle déduits sont remplacés. Cela crée une spécialisation qui ressemble à ceci:

template<>
struct has_member< A, void_t< decltype( A::member ) > > : true_type
{ };

Le type void_t< decltype( A::member ) >peut maintenant être évalué. Il est bien formé après la substitution, par conséquent, aucun échec de substitution ne se produit. On a:

template<>
struct has_member<A, void> : true_type
{ };

3. Choix

Maintenant , nous pouvons comparer la liste de paramètres de modèle de cette spécialisation avec les arguments de modèle fournis à l'original has_member<A>::value. Les deux types correspondent exactement, donc cette spécialisation partielle est choisie.


D'autre part, lorsque nous définissons le modèle comme:

template< class , class = int > // <-- int here instead of void
struct has_member : false_type
{ };

template< class T >
struct has_member< T , void_t< decltype( T::member ) > > : true_type
{ };

On se retrouve avec la même spécialisation:

template<>
struct has_member<A, void> : true_type
{ };

mais notre liste d'arguments de modèle pour l' has_member<A>::valueinstant est <A, int>. Les arguments ne correspondent pas aux paramètres de la spécialisation et le modèle principal est choisi comme solution de secours.


(*) La norme, à mon humble avis, inclut le processus de substitution et la correspondance des arguments de modèle explicitement spécifiés dans le processus de déduction des arguments de modèle . Par exemple (post-N4296) [temp.class.spec.match] / 2:

Une spécialisation partielle correspond à une liste d'arguments de modèle réelle donnée si les arguments de modèle de la spécialisation partielle peuvent être déduits de la liste d'arguments de modèle réelle.

Mais cela ne signifie pas seulement que tous les paramètres de modèle de la spécialisation partielle doivent être déduits; cela signifie également que la substitution doit réussir et (comme il semble?) les arguments du modèle doivent correspondre aux paramètres de modèle (substitués) de la spécialisation partielle. Notez que je ne sais pas complètement la norme spécifie la comparaison entre la liste d'arguments substitués et la liste d'arguments fournie.

dyp
la source
3
Je vous remercie! Je l'ai lu encore et encore, et je suppose que ma réflexion sur le fonctionnement exact de la déduction des arguments de modèle et sur ce que le compilateur choisit pour le modèle final n'est pas correcte pour le moment.
compensation
1
@ JohannesSchaub-litb Merci! C'est un peu déprimant, cependant. N'existe-t-il pas vraiment de règles pour faire correspondre un argument de modèle avec une spécialisation? Pas même pour les spécialisations explicites?
dyp
2
Arguments de modèle par défaut W / r / t, open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2008
TC
1
@dyp Quelques semaines plus tard et en lisant beaucoup à ce sujet et avec un indice de cet extrait, je pense que je commence à comprendre comment cela fonctionne. Votre explication fait de lire à lire plus de sens pour moi, merci!
nonsensation
1
Je voulais ajouter que le terme modèle principal était la clé (la première rencontre des modèles dans le code)
compensation
18
// specialized as has_member< T , void > or discarded (sfinae)
template<class T>
struct has_member<T , void_t<decltype(T::member)>> : true_type
{ };

Cette spécialisation ci-dessus n'existe que lorsqu'elle est bien formée, donc quand elle decltype( T::member )est valide et non ambiguë. la spécialisation est ainsi pour l' has_member<T , void>état dans le commentaire.

Lorsque vous écrivez has_member<A>, c'est à has_member<A, void>cause de l'argument de modèle par défaut.

Et nous avons une spécialisation pour has_member<A, void>(donc hériter de true_type) mais nous n'avons pas de spécialisation pour has_member<B, void>(nous utilisons donc la définition par défaut: hériter de false_type)

Jarod42
la source