La réponse acceptée à cette question de l'introspection des fonctions membres du temps de compilation, bien qu'elle soit à juste titre populaire, présente un hic qui peut être observé dans le programme suivant:
#include <type_traits>
#include <iostream>
#include <memory>
/* Here we apply the accepted answer's technique to probe for the
the existence of `E T::operator*() const`
*/
template<typename T, typename E>
struct has_const_reference_op
{
template<typename U, E (U::*)() const> struct SFINAE {};
template<typename U> static char Test(SFINAE<U, &U::operator*>*);
template<typename U> static int Test(...);
static const bool value = sizeof(Test<T>(0)) == sizeof(char);
};
using namespace std;
/* Here we test the `std::` smart pointer templates, including the
deprecated `auto_ptr<T>`, to determine in each case whether
T = (the template instantiated for `int`) provides
`int & T::operator*() const` - which all of them in fact do.
*/
int main(void)
{
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value << endl;
return 0;
}
Construit avec GCC 4.6.3, les sorties du programme 110
- nous informant que
T = std::shared_ptr<int>
ne fournit pasint & T::operator*() const
.
Si vous n'êtes pas déjà avisé de ce piège, alors un coup d'oeil à la définition de
std::shared_ptr<T>
dans l'en-tête vous <memory>
éclairera. Dans cette implémentation, std::shared_ptr<T>
est dérivé d'une classe de base dont il hérite operator*() const
. Ainsi, l'instanciation de modèle
SFINAE<U, &U::operator*>
qui constitue la "recherche" de l'opérateur pour
U = std::shared_ptr<T>
ne se produira pas, car elle std::shared_ptr<T>
n'a pas
operator*()
en soi et l'instanciation de modèle ne "fait pas d'héritage".
Cet hic n'affecte pas l'approche bien connue de SFINAE, utilisant "The sizeof () Trick", pour détecter simplement si T
a une fonction membre mf
(voir par exemple
cette réponse et commentaires). Mais établir ce qui T::mf
existe n'est souvent (généralement?) Pas assez bon: vous devrez peut-être également établir qu'il a la signature souhaitée. C'est là que la technique illustrée se démarque. La variante pointée de la signature souhaitée est inscrite dans un paramètre d'un type de modèle qui doit être satisfait par
&T::mf
pour que la sonde SFINAE réussisse. Mais cette technique d'instanciation de modèle donne la mauvaise réponse lorsqu'elle T::mf
est héritée.
Une technique SFINAE sûre pour l'introspection du temps de compilation de T::mf
doit éviter d'utiliser &T::mf
dans un argument de modèle pour instancier un type dont dépend la résolution de modèle de fonction SFINAE. Au lieu de cela, la résolution de la fonction de modèle SFINAE ne peut dépendre que des déclarations de type exactement pertinentes utilisées comme types d'argument de la fonction de sonde SFINAE surchargée.
En guise de réponse à la question qui respecte cette contrainte, je vais illustrer pour la détection à la compilation de E T::operator*() const
, pour arbitraire T
et E
. Le même modèle s'appliquera mutatis mutandis
à la recherche de toute autre signature de méthode membre.
#include <type_traits>
/*! The template `has_const_reference_op<T,E>` exports a
boolean constant `value that is true iff `T` provides
`E T::operator*() const`
*/
template< typename T, typename E>
struct has_const_reference_op
{
/* SFINAE operator-has-correct-sig :) */
template<typename A>
static std::true_type test(E (A::*)() const) {
return std::true_type();
}
/* SFINAE operator-exists :) */
template <typename A>
static decltype(test(&A::operator*))
test(decltype(&A::operator*),void *) {
/* Operator exists. What about sig? */
typedef decltype(test(&A::operator*)) return_type;
return return_type();
}
/* SFINAE game over :( */
template<typename A>
static std::false_type test(...) {
return std::false_type();
}
/* This will be either `std::true_type` or `std::false_type` */
typedef decltype(test<T>(0,0)) type;
static const bool value = type::value; /* Which is it? */
};
Dans cette solution, la fonction de sonde SFINAE surchargée test()
est "invoquée de manière récursive". (Bien sûr, il n'est pas du tout invoqué; il a simplement les types de retour des invocations hypothétiques résolues par le compilateur.)
Nous devons sonder au moins un et au plus deux points d'information:
T::operator*()
Existe- t- il du tout? Sinon, nous avons terminé.
- Étant donné que cela
T::operator*()
existe, est sa signature
E T::operator*() const
?
Nous obtenons les réponses en évaluant le type de retour d'un seul appel à test(0,0)
. Cela se fait par:
typedef decltype(test<T>(0,0)) type;
Cet appel peut être résolu en /* SFINAE operator-exists :) */
surcharge de test()
, ou il peut résoudre en /* SFINAE game over :( */
surcharge. Il ne peut pas résoudre la /* SFINAE operator-has-correct-sig :) */
surcharge, car celui-ci n'attend qu'un seul argument et nous en passons deux.
Pourquoi passons-nous deux? Simplement pour forcer la résolution à exclure
/* SFINAE operator-has-correct-sig :) */
. Le deuxième argument n'a pas d'autre signification.
Cet appel à test(0,0)
se résoudra au /* SFINAE operator-exists :) */
cas où le premier argument 0 satisferait le premier type de paramètre de cette surcharge, qui est decltype(&A::operator*)
, avec A = T
. 0 satisfera ce type juste au cas où il T::operator*
existe.
Supposons que le compilateur dise Oui à cela. Ensuite, il va avec
/* SFINAE operator-exists :) */
et il doit déterminer le type de retour de l'appel de fonction, qui dans ce cas est decltype(test(&A::operator*))
- le type de retour d'un autre appel à test()
.
Cette fois, nous ne passons qu'un seul argument, &A::operator*
dont nous savons maintenant qu'il existe, sinon nous ne serions pas là. Un appel à test(&A::operator*)
peut résoudre soit à, soit à /* SFINAE operator-has-correct-sig :) */
nouveau à pourrait résoudre à /* SFINAE game over :( */
. L'appel correspondra
/* SFINAE operator-has-correct-sig :) */
juste au cas où &A::operator*
satisfait le type de paramètre unique de cette surcharge, qui est E (A::*)() const
, avec A = T
.
Le compilateur dira Oui ici s'il T::operator*
a la signature souhaitée, puis à nouveau doit évaluer le type de retour de la surcharge. Plus de "récursions" maintenant: ça l'est std::true_type
.
Si le compilateur ne choisit pas /* SFINAE operator-exists :) */
pour l'appel test(0,0)
ou ne choisit pas /* SFINAE operator-has-correct-sig :) */
pour l'appel test(&A::operator*)
, dans les deux cas, il va avec
/* SFINAE game over :( */
et le type de retour final est std::false_type
.
Voici un programme de test qui montre le modèle produisant les réponses attendues dans un échantillon varié de cas (GCC 4.6.3 à nouveau).
// To test
struct empty{};
// To test
struct int_ref
{
int & operator*() const {
return *_pint;
}
int & foo() const {
return *_pint;
}
int * _pint;
};
// To test
struct sub_int_ref : int_ref{};
// To test
template<typename E>
struct ee_ref
{
E & operator*() {
return *_pe;
}
E & foo() const {
return *_pe;
}
E * _pe;
};
// To test
struct sub_ee_ref : ee_ref<char>{};
using namespace std;
#include <iostream>
#include <memory>
#include <vector>
int main(void)
{
cout << "Expect Yes" << endl;
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value;
cout << has_const_reference_op<std::vector<int>::iterator,int &>::value;
cout << has_const_reference_op<std::vector<int>::const_iterator,
int const &>::value;
cout << has_const_reference_op<int_ref,int &>::value;
cout << has_const_reference_op<sub_int_ref,int &>::value << endl;
cout << "Expect No" << endl;
cout << has_const_reference_op<int *,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,char &>::value;
cout << has_const_reference_op<unique_ptr<int>,int const &>::value;
cout << has_const_reference_op<unique_ptr<int>,int>::value;
cout << has_const_reference_op<unique_ptr<long>,int &>::value;
cout << has_const_reference_op<int,int>::value;
cout << has_const_reference_op<std::vector<int>,int &>::value;
cout << has_const_reference_op<ee_ref<int>,int &>::value;
cout << has_const_reference_op<sub_ee_ref,int &>::value;
cout << has_const_reference_op<empty,int &>::value << endl;
return 0;
}
Y a-t-il de nouveaux défauts dans cette idée? Peut-il être rendu plus générique sans tomber à nouveau sous le coup de l'accident qu'il évite?