Comment fonctionne l’implémentation de std :: is_function par Eric Niebler?

La semaine dernière, Eric Niebler a tweeté une implémentation très compacte pour la classe std::is_function :

 #include  template struct priority_tag : priority_tag {}; template struct priority_tag {}; // Function types here: template char(&is_function_impl_(priority_tag))[1]; // Array types here: template char(&is_function_impl_(priority_tag))[2]; // Anything that can be returned from a function here (including // void and reference types): template char(&is_function_impl_(priority_tag))[3]; // Classes and unions (including abstract types) here: template char(&is_function_impl_(priority_tag))[4]; template  struct is_function : std::integral_constant<bool, sizeof(is_function_impl_(priority_tag{})) == 1> {}; 

Mais comment ça fonctionne?

    L’idée générale

    Au lieu de répertorier tous les types de fonctions valides, comme l’ exemple d’implémentation sur cpprefereence.com , cette implémentation répertorie tous les types qui ne sont pas des fonctions, puis se résume à true si aucun n’est associé.

    La liste des types non fonctionnels comprend (de bas en haut):

    • Classes et unions (y compris les types abstraits)
    • Tout ce qui peut être renvoyé par une fonction (y compris les types de void et de référence)
    • Types de tableaux

    Un type qui ne correspond à aucun de ces types de non-fonction est un type de fonction. Notez que std::is_function considère explicitement que les types appelables comme lambdas ou les classes avec un opérateur d’appel de fonction ne sont pas des fonctions.

    is_function_impl_

    Nous fournissons une surcharge de la fonction is_function_impl pour chacun des types de non-fonction possibles. Les déclarations de fonctions peuvent être un peu difficiles à parsingr, nous allons donc les décomposer pour l’exemple du cas des classes et des unions :

     template char(&is_function_impl_(priority_tag<3>))[4]; 

    Cette ligne déclare un modèle de fonction is_function_impl_ qui prend un seul argument de type priority_tag<3> et renvoie une référence à un tableau de 4 caractères. Comme il est d’usage depuis les temps anciens de C, la syntaxe de la déclaration est horriblement compliquée par la présence de types de tableaux.

    Ce modèle de fonction prend deux arguments de modèle. Le premier est juste un T non contraint, mais le second est un pointeur sur un membre de T de type int . La partie int ici n’a pas vraiment d’importance, à savoir. cela fonctionnera même pour les T qui n’ont aucun membre de type int . Cependant, cela se traduit par une erreur de syntaxe pour les T qui ne sont pas de type classe ou union. Pour ces autres types, tenter d’instancier le modèle de fonction entraînera un échec de substitution.

    Des astuces similaires sont utilisées pour les surcharges priority_tag<2> et priority_tag<1> , qui utilisent leurs arguments de second modèle pour former des expressions qui ne sont compilées que pour T types de retour de fonction ou les types de tableau valides. Seule la surcharge priority_tag<0> n’a pas un second paramètre de gabarit contraignant et peut donc être instanciée avec n’importe quel T

    is_function_impl_ , nous déclarons quatre surcharges différentes pour is_function_impl_ , qui diffèrent par leur argument d’entrée et leur type de retour. Chacun d’eux prend un argument de type priority_tag différent et renvoie une référence à un tableau de caractères de taille unique différente.

    is_function tag dans is_function

    Maintenant, lors de l’instanciation de is_function , il instancie is_function_impl avec T Notez que puisque nous avons fourni quatre surcharges différentes pour cette fonction, la résolution de la surcharge doit avoir lieu ici. Et comme toutes ces surcharges sont des modèles de fonction, cela signifie que SFINAE a une chance de se lancer.

    Donc, pour les fonctions (et uniquement les fonctions), toutes les surcharges échoueront sauf la plus générale avec priority_tag<0> . Alors, pourquoi l’instanciation ne résout-elle pas toujours cette surcharge, si c’est la plus générale? En raison des arguments en entrée de nos fonctions surchargées.

    Notez que priority_tag est construit de telle manière que priority_tag hérite publiquement de priority_tag . Maintenant que is_function_impl est is_function_impl ici avec priority_tag<3> , cette surcharge correspond mieux que les autres à la résolution de la surcharge, elle sera donc essayée en premier. Seulement si cela échoue à cause d’une erreur de substitution, la meilleure correspondance est essayée, qui est la surcharge priority_tag<2> . Nous continuons ainsi jusqu’à ce que nous trouvions soit une surcharge pouvant être instanciée, soit nous atteignons priority_tag<0> , qui n’est pas contraint et fonctionnera toujours. Comme tous les types de non-fonction sont couverts par les surcharges prio plus élevées, cela ne peut se produire que pour les types de fonctions.

    Evaluer le résultat

    Nous examinons maintenant la taille du type renvoyé par l’appel à is_function_impl_ pour évaluer le résultat. N’oubliez pas que chaque surcharge renvoie une référence à un tableau de caractères de taille différente. Nous pouvons donc utiliser sizeof pour vérifier quelle surcharge a été sélectionnée et définir uniquement le résultat sur true si nous avons atteint la surcharge priority_tag<0> .

    Bugs connus

    Johannes Schaub a trouvé un bug dans l’implémentation. Un tableau de type de classe incomplet sera incorrectement classé en tant que fonction. Cela est dû au fait que le mécanisme de détection actuel des types de tableau ne fonctionne pas avec des types incomplets.