2016-02-04 1 views
9

J'essaie de comprendre la mise en œuvre de std::is_class. J'ai copié quelques implémentations possibles et les ai compilées, dans l'espoir de comprendre comment elles fonctionnent. Cela fait, je trouve que tous les calculs sont faits pendant la compilation (comme j'aurais dû le découvrir plus tôt, en regardant en arrière), donc gdb ne peut pas me donner plus de détails sur ce qui se passe exactement.Comment fonctionne cette implémentation de std :: is_class?

La mise en œuvre que j'ai du mal à comprendre est celui-ci:

template<class T, T v> 
    struct integral_constant{ 
    static constexpr T value = v; 
    typedef T value_type; 
    typedef integral_constant type; 
    constexpr operator value_type() const noexcept { 
     return value; 
    } 
}; 

namespace detail { 
    template <class T> char test(int T::*); //this line 
    struct two{ 
     char c[2]; 
    }; 
    template <class T> two test(...);   //this line 
} 

//Not concerned about the is_union<T> implementation right now 
template <class T> 
struct is_class : std::integral_constant<bool, sizeof(detail::test<T>(0))==1 
                && !std::is_union<T>::value> {}; 

Je vais avoir du mal avec les deux lignes de commentaires. Cette première ligne:

template<class T> char test(int T::*); 

Que signifie le T::*? Aussi, n'est-ce pas une déclaration de fonction? Cela ressemble à un, mais cela compile sans définir un corps de fonction.

La deuxième ligne que je veux comprendre est:

template<class T> two test(...); 

Encore une fois, est-ce pas une déclaration de fonction sans corps jamais défini? Que veut dire l'ellipse dans ce contexte? Je pensais qu'une ellipse en tant qu'argument de la fonction nécessitait un argument défini avant le ...?

Je voudrais comprendre ce que fait ce code. Je sais que je peux simplement utiliser les fonctions déjà implémentées de la bibliothèque standard, mais je veux comprendre comment elles fonctionnent.

Références:

+0

Les fonctions * déclarations * ne contiennent pas de corps. C'est pour les définitions. –

+0

Définition des fonctions ARE une déclaration de fonction, Karoly. Le nit que vous choisissez est que toutes les définitions sont des déclarations, mais toutes les déclarations ne sont pas des définitions. – Peter

Répondre

8

Ce que vous regardez est une technologie de programmation appelée "SFINAE" qui signifie "L'échec de substitution n'est pas une erreur". L'idée de base est la suivante:

namespace detail { 
    template <class T> char test(int T::*); //this line 
    struct two{ 
    char c[2]; 
    }; 
    template <class T> two test(...);   //this line 
} 

Cet espace de noms fournit 2 pour test() surcharges. Les deux sont des modèles, résolus au moment de la compilation. Le premier prend un argument int T::*. Il s'appelle un Member-Pointer et est un pointeur vers un int, mais vers un int thats un membre de la classe T. Ceci est seulement une expression valide, si T est une classe. La seconde prend n'importe quel nombre d'arguments, ce qui est valide dans tous les cas.

Alors, comment est-il utilisé?

sizeof(detail::test<T>(0))==1 

Ok, on passe la fonction 0 - cela peut être un pointeur et surtout un membre-pointeur - aucune information qui a gagné la surcharge d'utiliser de cette situation. Donc, si T est une classe, alors nous pourrions utiliser à la fois la T::* et la surcharge ... ici - et puisque la surcharge T::* est la plus spécifique ici, elle est utilisée. Mais si T n'est pas une classe, alors nous ne pouvons pas avoir quelque chose comme T::* et la surcharge est mal formée. Mais c'est un échec qui s'est produit pendant la substitution de paramètres de template. Et puisque "les échecs de substitution ne sont pas une erreur" le compilateur ignorera silencieusement cette surcharge.

Ensuite, le sizeof() est appliqué. Remarqué les différents types de retour?Donc, en fonction de T le compilateur choisit la bonne surcharge et donc le bon type de retour, résultant en une taille de sizeof(char) ou sizeof(char[2]).

Et enfin, puisque nous n'utilisons que la taille de cette fonction et que nous ne l'appelons jamais, nous n'avons pas besoin d'une implémentation.

1

Qu'est-ce que le T::* signifie? Aussi, n'est-ce pas une déclaration de fonction? Cela ressemble à un, mais cela compile sans définir un corps de fonction.

Le int T::* est un pointer to member object. Il peut être utilisé comme suit:

struct T { int x; } 
int main() { 
    int T::* ptr = &T::x; 

    T a {123}; 
    a.*ptr = 0; 
} 

Encore une fois, est-ce pas une déclaration de fonction sans corps jamais défini? Que veut dire l'ellipse dans ce contexte?

Dans l'autre ligne:

template<class T> two test(...); 

l'ellipse is a C construct pour définir une fonction qui prend un certain nombre d'arguments.

Je voudrais comprendre ce que fait ce code.

Fondamentalement, il y a vérifier si un type spécifique est un struct ou un class en vérifiant si 0 peut être interprété comme un pointeur membre (auquel cas T est un type de classe).

Plus précisément, dans ce code:

namespace detail { 
    template <class T> char test(int T::*); 
    struct two{ 
     char c[2]; 
    }; 
    template <class T> two test(...); 
} 

vous avez deux surcharges:

  • qui n'a d'égal que lorsqu'un T est un type de classe (dans ce cas, celui-ci est le meilleur match et « victoires » sur la deuxième)
  • sur qui est adapté chaque fois

Dans le premier sizeof le résultat donne 1 (le type de retour de la fonction est char), l'autre renvoie 2 (une structure contenant 2 caractères).

La valeur booléenne vérifié est alors:

sizeof(detail::test<T>(0)) == 1 && !std::is_union<T>::value 

qui signifie: retourner true que si le 0 constante intégrale peut être interprété comme un pointeur vers membre de type T (auquel cas il est un type de classe), mais ce n'est pas un union (qui est aussi un type de classe possible).

1

Test est une fonction surchargée qui prend un pointeur vers un membre dans T ou quoi que ce soit. C++ exige que la meilleure correspondance soit utilisée. Donc, si T est un type de classe peut peut avoir un membre dedans ... alors cette version est sélectionnée et la taille de son retour est 1. Si T n'est pas un type de classe alors T :: * fait zéro sens pour que La version de la fonction est filtrée par SFINAE et ne sera pas disponible. La version de n'importe quoi est utilisée et sa taille de type de retour n'est pas 1. Ainsi, vérifier la taille du retour d'appel de cette fonction permet de décider si le type peut avoir des membres ... il ne reste plus qu'à s'assurer que ce n'est pas un syndicat si c'est une classe ou pas.

8

Une partie de ce qui vous rend confus, ce qui n'est pas expliqué par les autres réponses jusqu'à présent, est que les fonctions test ne sont jamais réellement appelées. Le fait qu'ils n'ont pas de définitions n'a pas d'importance si vous ne les appelez pas. Comme vous l'avez compris, tout se passe au moment de la compilation, sans exécuter de code.

L'expression sizeof(detail::test<T>(0)) utilise l'opérateur sizeof sur une expression d'appel de fonction. L'opérande de sizeof est un contexte non évalué, ce qui signifie que le compilateur n'exécute pas réellement ce code (c'est-à-dire l'évalue pour déterminer le résultat). Il n'est pas nécessaire d'appeler cette fonction afin de connaître le sizeof ce que le résultat serait être si vous l'appeliez. Pour connaître la taille du résultat, le compilateur n'a besoin que de voir les déclarations des différentes fonctions test (pour connaître leurs types de retour), puis d'effectuer une résolution de surcharge pour voir laquelle serait être appelée, et donc de trouver ce que le sizeof le résultat serait be.

Le reste du casse-tête est que l'appel de fonction non évaluée detail::test<T>(0) détermine si T peut être utilisé pour former un type pointeur à membre int T::*, qui est seulement possible si T est un type de classe (parce que les non-classes peuvent » t avoir des membres, et ne peut donc pas avoir de pointeurs sur leurs membres). Si T est une classe, la première surcharge test peut être appelée, sinon la seconde surcharge est appelée. La deuxième surcharge utilise une liste de paramètres printf -style ..., ce qui signifie qu'elle accepte tout, mais est également considérée comme une pire correspondance que toute autre fonction viable (sinon les fonctions utilisant ... seraient trop "gourmandes" et seraient appelées tout le temps , même s'il existe une fonction plus spécifique qui correspond exactement aux arguments). Dans ce code, la fonction ... est un repli pour "si rien d'autre ne correspond, appelez cette fonction", donc si T n'est pas un type de classe, la méthode de repli est utilisée. Peu importe si le type de classe a vraiment une variable membre de type int, il est correct de former le type int T::* de toute façon pour n'importe quelle classe (vous ne pouviez pas faire référence au pointeur à un membre membre si le type n'a pas de membre int).

+1

plus 1 pour les trois dernières lignes. –