Qu'est-ce que SFINAE en C++ ?
Pouvez-vous l'expliquer dans des termes compréhensibles pour un programmeur qui n'est pas versé dans le C++ ? En outre, à quel concept SFINAE correspond-il dans un langage comme Python ?
Qu'est-ce que SFINAE en C++ ?
Pouvez-vous l'expliquer dans des termes compréhensibles pour un programmeur qui n'est pas versé dans le C++ ? En outre, à quel concept SFINAE correspond-il dans un langage comme Python ?
Avertissement : ceci est un vraiment longue explication, mais j'espère qu'elle explique vraiment non seulement ce que fait SFINAE, mais donne une idée de quand et pourquoi vous pourriez l'utiliser.
Bon, pour expliquer cela, nous devons probablement revenir en arrière et expliquer un peu les modèles. Comme nous le savons tous, Python utilise ce que l'on appelle communément le typage en canard -- par exemple, lorsque vous invoquez une fonction, vous pouvez passer un objet X à cette fonction tant que X fournit toutes les opérations utilisées par la fonction.
En C++, une fonction normale (sans modèle) exige que vous spécifiiez le type d'un paramètre. Si vous définissez une fonction comme :
int plus1(int x) { return x + 1; }
Vous pouvez sólo appliquer cette fonction à un int
. Le fait qu'il utilise x
d'une manière qui pourrait s'applique aussi bien à d'autres types d'objets comme long
o float
ne fait aucune différence -- elle ne s'applique qu'à un int
de toute façon.
Pour obtenir quelque chose de plus proche du typage en canard de Python, vous pouvez créer un modèle à la place :
template <class T>
T plus1(T x) { return x + 1; }
Maintenant, notre plus1
ressemble beaucoup plus à ce qu'il serait en Python -- en particulier, nous pouvons l'invoquer aussi bien à un objet x
de tout type pour lequel x + 1
est définie.
Maintenant, considérons, par exemple, que nous voulons écrire certains objets dans un flux. Malheureusement, certains de ces objets sont écrits dans un flux en utilisant la commande stream << object
mais d'autres utilisent object.write(stream);
à la place. Nous voulons être en mesure de gérer l'un ou l'autre sans que l'utilisateur ait à préciser lequel. Maintenant, la spécialisation des modèles nous permet d'écrire le modèle spécialisé, donc si c'était un qui utilisait le object.write(stream)
syntaxe, on pourrait faire quelque chose comme :
template <class T>
std::ostream &write_object(T object, std::ostream &os) {
return os << object;
}
template <>
std::ostream &write_object(special_object object, std::ostream &os) {
return object.write(os);
}
C'est bien pour un seul type, et si nous le voulions vraiment, nous pourrions ajouter d'autres spécialisations pour les todo les types qui ne supportent pas stream << object
-- mais dès que (par exemple) l'utilisateur ajoute un nouveau type qui ne supporte pas stream << object
les choses se cassent à nouveau.
Ce que nous voulons c'est un moyen d'utiliser la première spécialisation pour tout objet qui supporte stream << object;
mais le second pour tout le reste (bien que nous puissions parfois vouloir en ajouter un troisième pour les objets qui utilisent la fonction x.print(stream);
à la place).
Nous pouvons utiliser SFINAE pour faire cette détermination. Pour ce faire, nous nous appuyons généralement sur quelques autres détails bizarres du C++. L'un d'entre eux consiste à utiliser la fonction sizeof
opérateur. sizeof
détermine la taille d'un type ou d'une expression, mais il le fait entièrement au moment de la compilation, en examinant l'élément types impliquée, sans évaluer l'expression elle-même. Par exemple, si j'ai quelque chose comme :
int func() { return -1; }
Je peux utiliser sizeof(func())
. Dans ce cas, func()
renvoie un int
donc sizeof(func())
est équivalent à sizeof(int)
.
Le deuxième élément intéressant et fréquemment utilisé est le fait que la taille d'un tableau doit être positive, no zéro.
Maintenant, en les mettant ensemble, on peut faire quelque chose comme ça :
// stolen, more or less intact from:
// http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles
template<class T> T& ref();
template<class T> T val();
template<class T>
struct has_inserter
{
template<class U>
static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]);
template<class U>
static long test(...);
enum { value = 1 == sizeof test<T>(0) };
typedef boost::integral_constant<bool, value> type;
};
Ici, nous avons deux surcharges de test
. La seconde prend une liste d'arguments variables (le paramètre ...
), ce qui signifie qu'elle peut correspondre à n'importe quel type -- mais c'est aussi le dernier choix que fera le compilateur en sélectionnant une surcharge, de sorte qu'elle sera sólo si le premier correspond no . L'autre surcharge de test
est un peu plus intéressant : il définit une fonction qui prend un paramètre : un tableau de pointeurs vers des fonctions qui retournent char
où la taille du tableau est (essentiellement) sizeof(stream << object)
. Si stream << object
n'est pas une expression valide, le sizeof
donnera 0, ce qui signifie que nous avons créé un tableau de taille zéro, ce qui n'est pas autorisé. C'est là que le SFINAE lui-même entre en jeu. En essayant de substituer le type qui ne supporte pas la fonction operator<<
para U
échouerait, car il produirait un tableau de taille nulle. Mais, ce n'est pas une erreur -- cela signifie simplement que cette fonction est éliminée de l'ensemble des surcharges. Par conséquent, l'autre fonction est la seule qui peut être utilisée dans un tel cas.
Il est ensuite utilisé dans le enum
ci-dessous - elle examine la valeur de retour de la surcharge sélectionnée de la fonction test
et vérifie s'il est égal à 1 (s'il l'est, cela signifie que la fonction renvoyant char
a été sélectionné, mais sinon, la fonction renvoyant long
a été sélectionné).
Le résultat est que has_inserter<type>::value
sera l
si nous pouvions utiliser some_ostream << object;
compilerait, et 0
si ce n'était pas le cas. Nous pouvons ensuite utiliser cette valeur pour contrôler la spécialisation du modèle afin de choisir la bonne façon d'écrire la valeur pour un type particulier.
Si vous avez des fonctions de gabarit surchargées, certains des candidats possibles à l'utilisation peuvent ne pas être compilables lorsque la substitution de gabarit est effectuée, parce que la chose substituée peut ne pas avoir le comportement correct. Ceci n'est pas considéré comme une erreur de programmation, les modèles qui ont échoué sont simplement retirés de l'ensemble disponible pour ce paramètre particulier.
Je ne sais pas si Python dispose d'une fonctionnalité similaire, et je ne vois pas vraiment pourquoi un programmeur non-C++ devrait s'intéresser à cette fonctionnalité. Mais si vous voulez en savoir plus sur les templates, le meilleur livre à ce sujet est le suivant Modèles C++ : Le guide complet .
SFINAE est un principe que le compilateur C++ utilise pour filtrer certaines surcharges de fonctions modélisées pendant la résolution des surcharges (1).
Lorsque le compilateur résout un appel de fonction particulier, il considère un ensemble de déclarations de fonctions et de modèles de fonctions disponibles pour savoir laquelle sera utilisée. Fondamentalement, il y a deux mécanismes pour le faire. L'un peut être décrit comme syntaxique. Déclarations données :
template <class T> void f(T); //1
template <class T> void f(T*); //2
template <class T> void f(std::complex<T>); //3
résolution de f((int)1)
supprimera les versions 2 et 3, car int
n'est pas égal à complex<T>
o T*
pour certains T
. De même, f(std::complex<float>(1))
supprimerait la deuxième variante et f((int*)&x)
supprimerait le troisième. Le compilateur fait cela en essayant de déduire les paramètres du modèle à partir des arguments de la fonction. Si la déduction échoue (comme dans T*
contre int
), la surcharge est rejetée.
La raison pour laquelle nous voulons cela est évidente - nous pouvons vouloir faire des choses légèrement différentes pour différents types (par exemple, la valeur absolue d'un complexe est calculée par x*conj(x)
et donne un nombre réel, pas un nombre complexe, ce qui est différent du calcul pour les flottants).
Si vous avez déjà fait de la programmation déclarative, ce mécanisme est similaire à (Haskell) :
f Complex x y = ...
f _ = ...
La façon dont le C++ va plus loin est que la déduction peut échouer même si les types déduits sont corrects, mais que la rétro-substitution dans l'autre donne un résultat "absurde" (plus sur ce point plus tard). Par exemple :
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
en déduisant f('c')
(nous appelons avec un seul argument, car le deuxième argument est implicite) :
T
contre char
ce qui donne trivialement T
comme char
T
dans la déclaration comme char
s. On obtient ainsi void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
. La taille de ce tableau peut être par exemple de -3 (en fonction de votre plateforme).<= 0
sont invalides, donc le compilateur rejette la surcharge. L'échec de la substitution n'est pas une erreur le compilateur ne rejettera pas le programme.Au final, s'il reste plus d'une surcharge de fonction, le compilateur utilise la comparaison des séquences de conversion et l'ordonnancement partiel des modèles pour en sélectionner une qui est la "meilleure".
Il existe d'autres résultats "absurdes" qui fonctionnent de cette manière, ils sont énumérés dans une liste dans la norme (C++03). En C++0x, le domaine de la SFINAE est étendu à presque toutes les erreurs de type.
Je n'écrirai pas une liste exhaustive des erreurs SFINAE, mais certaines des plus populaires sont les suivantes :
typename T::type
para T = int
o T = A
donde A
est une classe sans type imbriqué appelée type
.int C::*
para C = int
Ce mécanisme ne ressemble à rien dans les autres langages de programmation que je connais. Si vous deviez faire une chose similaire en Haskell, vous utiliseriez des gardes qui sont plus puissantes, mais impossibles en C++.
1 : ou des spécialisations partielles de modèles lorsqu'on parle de modèles de classes
Python ne vous aidera pas du tout. Mais vous dites que vous êtes déjà familier avec les templates.
La construction la plus fondamentale de SFINAE est l'utilisation de enable_if
. La seule partie délicate est que class enable_if
n'est pas encapsuler SFINAE, elle ne fait que l'exposer.
template< bool enable >
class enable_if { }; // enable_if contains nothing…
template<>
class enable_if< true > { // … unless argument is true…
public:
typedef void type; // … in which case there is a dummy definition
};
template< bool b > // if "b" is true,
typename enable_if< b >::type function() {} //the dummy exists: success
template< bool b >
typename enable_if< ! b >::type function() {} // dummy does not exist: failure
/* But Substitution Failure Is Not An Error!
So, first definition is used and second, although redundant and
nonsensical, is quietly ignored. */
int main() {
function< true >();
}
Dans la SFINAE, il existe une structure qui établit une condition d'erreur ( class enable_if
ici) et un certain nombre de définitions parallèles, par ailleurs contradictoires. Une erreur se produit dans toutes les définitions sauf une, que le compilateur choisit et utilise sans se plaindre des autres.
Les types d'erreurs acceptables sont un détail important qui n'a été normalisé que récemment, mais vous ne semblez pas poser de questions à ce sujet.
Il n'y a rien dans Python qui ressemble de près ou de loin à SFINAE. Python n'a pas de modèles, et certainement pas de résolution de fonction basée sur les paramètres comme cela se produit lors de la résolution des spécialisations de modèles. En Python, la recherche de fonctions se fait uniquement par nom.
Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.