web-dev-qa-db-fra.com

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

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

#include <type_traits>

template<int I> struct priority_tag : priority_tag<I - 1> {};
template<> struct priority_tag<0> {};

// Function types here:
template<typename T>
char(&is_function_impl_(priority_tag<0>))[1];

// Array types here:
template<typename T, typename = decltype((*(T*)0)[0])>
char(&is_function_impl_(priority_tag<1>))[2];

// Anything that can be returned from a function here (including
// void and reference types):
template<typename T, typename = T(*)()>
char(&is_function_impl_(priority_tag<2>))[3];

// Classes and unions (including abstract types) here:
template<typename T, typename = int T::*>
char(&is_function_impl_(priority_tag<3>))[4];

template <typename T>
struct is_function
    : std::integral_constant<bool, sizeof(is_function_impl_<T>(priority_tag<3>{})) == 1>
{};

Mais comment ça marche?

54
ComicSansMS

L'idée générale

Au lieu de répertorier tous les types de fonctions valides, comme le exemple d'implémentation sur cpprefereence.com , cette implémentation répertorie tous les types qui ne sont pas fonctionne, puis ne se résout en true que si aucune d'entre elles ne correspond.

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

  • Classes et unions (y compris les types abstraits)
  • Tout ce qui peut être renvoyé par une fonction (y compris void et types 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 les types appelables comme les lambdas ou les classes avec un opérateur d'appel de fonction comme 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 à analyser, alors décomposons-les pour l'exemple du cas classes et unions :

template<typename T, typename = int T::*>
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 chars. Comme c'est la coutume depuis les temps anciens de C, la syntaxe de 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 simplement 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, c'est-à-dire. cela fonctionnera même pour les T qui n'ont pas de membres de type int. Ce qu'il fait cependant, c'est qu'il en résultera une erreur de syntaxe pour les Ts qui ne sont pas de type classe ou union. Pour ces autres types, la tentative d'instanciation du 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 deuxièmes arguments de modèle pour former des expressions qui se compilent uniquement pour Ts étant respectivement des types de retour de fonction valides ou des types de tableau. Seule la surcharge priority_tag<0> N'a pas un deuxième paramètre de modèle aussi contraignant et peut donc être instanciée avec n'importe quel T.

Dans l'ensemble, 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 type priority_tag Différent comme argument et renvoie une référence à un tableau de caractères de taille unique différente.

Répartition des balises 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 surcharge doit avoir lieu ici. Et comme toutes ces surcharges sont des modèles de fonction , cela signifie SFINAE a un chance de se lancer.

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

Notez que priority_tag Est construit de telle manière que priority_tag<N+1> Hérite publiquement de priority_tag<N>. Maintenant, puisque is_function_impl Est invoqué ici avec priority_tag<3>, Cette surcharge est une meilleure correspondance que les autres pour la résolution de surcharge , il sera donc essayé en premier. Ce n'est qu'en cas d'échec en raison d'une erreur de substitution que la meilleure correspondance suivante est tentée, à savoir la surcharge priority_tag<2>. Nous continuons de cette manière jusqu'à ce que nous trouvions une surcharge pouvant être instanciée ou que nous atteignions priority_tag<0>, Ce qui n'est pas contraint et fonctionnera toujours. Étant donné que 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 fonction.

Évaluer le résultat

Nous inspectons maintenant la taille du type retourné 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 ne définir le résultat sur true que 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. En effet, le mécanisme de détection actuel des types de tableau ne fonctionne pas avec les types incomplets.

51
ComicSansMS