A resposta aceita para esta questão da introspecção de função membro de compilação, embora seja bastante popular, tem um problema que pode ser observado no seguinte programa:
#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;
}
Construído com GCC 4.6.3, os resultados do programa 110
- informando-nos que
T = std::shared_ptr<int>
não não fornecer int & T::operator*() const
.
Se você ainda não é sábio quanto a essa pegadinha, uma olhada na definição de
std::shared_ptr<T>
no cabeçalho <memory>
lançará luz. Nessa implementação, std::shared_ptr<T>
é derivado de uma classe base da qual ela herda operator*() const
. Portanto, a instanciação de modelo
SFINAE<U, &U::operator*>
que constitui "encontrar" o operador
U = std::shared_ptr<T>
não ocorrerá, porque std::shared_ptr<T>
não tem
operator*()
por si só e a instanciação de modelo não "faz herança".
Esse problema não afeta a conhecida abordagem SFINAE, usando "The sizeof () Trick", para detectar apenas se T
tem alguma função de membro mf
(veja, por exemplo,
esta resposta e comentários). Mas estabelecer que T::mf
existe muitas vezes (geralmente?) Não é bom o suficiente: você também pode precisar estabelecer que possui a assinatura desejada. É aí que a técnica ilustrada é pontuada. A variante apontada da assinatura desejada é inscrita em um parâmetro de um tipo de modelo que deve ser satisfeito
&T::mf
para que o probe SFINAE seja bem-sucedido. Mas essa técnica de instanciação de modelo fornece a resposta errada quando T::mf
é herdada.
Uma técnica SFINAE segura para introspecção de tempo de compilação T::mf
deve evitar o uso de &T::mf
um argumento de modelo para instanciar um tipo do qual depende a resolução do modelo de função SFINAE. Em vez disso, a resolução da função de modelo SFINAE pode depender apenas de declarações de tipo exatamente pertinentes usadas como tipos de argumento da função de teste SFINAE sobrecarregada.
Como resposta à pergunta que cumpre essa restrição, ilustrarei a detecção de compilações de E T::operator*() const
, arbitrárias T
e E
. O mesmo padrão se aplicará mutatis mutandis
para investigar qualquer outra assinatura de método membro.
#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? */
};
Nesta solução, a função de sonda SFINAE sobrecarregada test()
é "invocada recursivamente". (É claro que na verdade não é invocado; apenas possui os tipos de retorno de invocações hipotéticas resolvidos pelo compilador.)
Precisamos investigar pelo menos um e no máximo dois pontos de informação:
- Existe
T::operator*()
mesmo? Caso contrário, terminamos.
- Dado que
T::operator*()
existe, é a sua assinatura
E T::operator*() const
?
Obtemos as respostas avaliando o tipo de retorno de uma única chamada para test(0,0)
. Isso é feito por:
typedef decltype(test<T>(0,0)) type;
Essa chamada pode ser resolvida com a /* SFINAE operator-exists :) */
sobrecarga de test()
ou com a /* SFINAE game over :( */
sobrecarga. Não pode resolver a /* SFINAE operator-has-correct-sig :) */
sobrecarga, porque esse espera apenas um argumento e estamos passando dois.
Por que estamos passando dois? Simplesmente para forçar a resolução a excluir
/* SFINAE operator-has-correct-sig :) */
. O segundo argumento não tem outro significado.
Essa chamada para test(0,0)
resolverá /* SFINAE operator-exists :) */
apenas no caso de o primeiro argumento 0 satisfazer o primeiro tipo de parâmetro dessa sobrecarga, ou seja decltype(&A::operator*)
, com A = T
. 0 satisfará esse tipo, caso T::operator*
exista.
Vamos supor que o compilador diga Sim para isso. Então, ele continua
/* SFINAE operator-exists :) */
e precisa determinar o tipo de retorno da chamada de função, que nesse caso é decltype(test(&A::operator*))
- o tipo de retorno de mais uma chamada para test()
.
Desta vez, estamos passando apenas um argumento, &A::operator*
que sabemos que existe, ou não estaríamos aqui. Uma chamada para test(&A::operator*)
pode resolver para /* SFINAE operator-has-correct-sig :) */
ou novamente para /* SFINAE game over :( */
. A chamada corresponderá
/* SFINAE operator-has-correct-sig :) */
caso &A::operator*
satisfaça o tipo de parâmetro único dessa sobrecarga, ou seja E (A::*)() const
, com A = T
.
O compilador dirá Sim aqui se T::operator*
tiver a assinatura desejada e, em seguida, novamente terá que avaliar o tipo de retorno da sobrecarga. Não há mais "recursões" agora: é std::true_type
.
Se o compilador não escolhe /* SFINAE operator-exists :) */
para a chamada test(0,0)
ou não escolher /* SFINAE operator-has-correct-sig :) */
para a chamada test(&A::operator*)
, em seguida, em ambos os casos ele vai com
/* SFINAE game over :( */
e o tipo de retorno final é std::false_type
.
Aqui está um programa de teste que mostra o modelo produzindo as respostas esperadas em uma amostra variada de casos (GCC 4.6.3 novamente).
// 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;
}
Existem novas falhas nessa idéia? Pode ser tornado mais genérico sem, mais uma vez, cair no obstáculo que evita?