Przyjęta odpowiedź na to pytanie o introspekcję funkcji składowych w czasie kompilacji, chociaż jest słusznie popularna, ma pewien problem, który można zaobserwować w następującym programie:
#include <type_traits>
#include <iostream>
#include <memory>
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;
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;
}
Zbudowany z GCC 4.6.3, wyjść programowych 110
- informując nas, że
T = std::shared_ptr<int>
ma nie zapewniająint & T::operator*() const
.
Jeśli nie jesteś jeszcze mądry w tej kwestii, to spojrzenie na definicję
std::shared_ptr<T>
w nagłówku <memory>
rzuci światło. W tej implementacji std::shared_ptr<T>
pochodzi z klasy bazowej, z której dziedziczy operator*() const
. Zatem instancja szablonu, SFINAE<U, &U::operator*>
która polega na
„znalezieniu” operatora dla
U = std::shared_ptr<T>
, nie nastąpi, ponieważ std::shared_ptr<T>
ma
operator*()
własne prawo, a instancja szablonu nie „dziedziczy”.
Ten szkopuł nie wpływa na dobrze znane podejście SFINAE, wykorzystujące "Sztukę sizeof ()", do wykrywania jedynie, czy T
ma jakąś funkcję składową mf
(patrz np.
Ta odpowiedź i komentarze). Ale ustalenie, że T::mf
istnieje, często (zwykle?) Nie jest wystarczające: może być również konieczne ustalenie, czy ma on pożądany podpis. To jest miejsce, w którym zilustrowana technika zdobywa punkty. Wskazany wariant żądanego podpisu jest wpisany w parametr typu szablonu, który musi zostać spełniony,
&T::mf
aby sonda SFINAE zakończyła się powodzeniem. Ale ta technika tworzenia wystąpienia szablonu daje błędną odpowiedź, gdy T::mf
jest dziedziczona.
Bezpieczna technika SFINAE do introspekcji w czasie kompilacji T::mf
musi unikać stosowania&T::mf
argumentu szablonu w celu utworzenia instancji typu, od którego zależy rozdzielczość szablonu funkcji SFINAE. Zamiast tego rozdzielczość funkcji szablonu SFINAE może zależeć tylko od dokładnie odpowiednich deklaracji typu używanych jako typy argumentów przeciążonej funkcji sondy SFINAE.
Odpowiadając na pytanie, które podlega temu ograniczeniu, zilustruję dla wykrywania w czasie kompilacji E T::operator*() const
, arbitralnych T
i E
. Ten sam wzorzec będzie stosowany mutatis mutandis
do sondowania dla dowolnej innej sygnatury metody składowej.
#include <type_traits>
template< typename T, typename E>
struct has_const_reference_op
{
template<typename A>
static std::true_type test(E (A::*)() const) {
return std::true_type();
}
template <typename A>
static decltype(test(&A::operator*))
test(decltype(&A::operator*),void *) {
typedef decltype(test(&A::operator*)) return_type;
return return_type();
}
template<typename A>
static std::false_type test(...) {
return std::false_type();
}
typedef decltype(test<T>(0,0)) type;
static const bool value = type::value;
};
W tym rozwiązaniu przeciążona funkcja sondy SFINAE test()
jest „wywoływana rekurencyjnie”. (Oczywiście w rzeczywistości nie jest w ogóle wywoływana; ma jedynie zwracane typy hipotetycznych wywołań rozwiązanych przez kompilator).
Musimy zbadać co najmniej jeden, a co najwyżej dwa punkty informacji:
- Czy
T::operator*()
w ogóle istnieje? Jeśli nie, to koniec.
- Biorąc pod uwagę, że
T::operator*()
istnieje, czy jego podpis
E T::operator*() const
?
Odpowiedzi uzyskujemy, oceniając typ zwracania pojedynczego wywołania test(0,0)
. Robi się to przez:
typedef decltype(test<T>(0,0)) type;
To wywołanie może zostać rozwiązane jako /* SFINAE operator-exists :) */
przeciążenie test()
lub może zostać rozwiązane jako /* SFINAE game over :( */
przeciążenie. Nie może rozwiązać problemu z /* SFINAE operator-has-correct-sig :) */
przeciążeniem, ponieważ ten oczekuje tylko jednego argumentu, a my przekazujemy dwa.
Dlaczego mijamy dwóch? Wystarczy wymusić wyłączenie w rezolucji
/* SFINAE operator-has-correct-sig :) */
. Drugi argument nie ma innego znaczenia.
To wywołanie test(0,0)
zostanie rozwiązane na /* SFINAE operator-exists :) */
wypadek, gdyby pierwszy argument 0 spełniał pierwszy typ parametru tego przeciążenia, czyli decltype(&A::operator*)
with A = T
. 0 spełni ten typ na wszelki wypadek T::operator*
.
Załóżmy, że kompilator powie na to tak. Potem idzie dalej
/* SFINAE operator-exists :) */
i musi określić typ zwracania wywołania funkcji, którym w tym przypadku jest decltype(test(&A::operator*))
- typ zwracania jeszcze jednego wywołania funkcji test()
.
Tym razem mijamy tylko jeden argument, o &A::operator*
którym teraz wiemy, że istnieje lub nie byłoby nas tutaj. Wezwanie do test(&A::operator*)
może rozwiązać się /* SFINAE operator-has-correct-sig :) */
albo ponownie, albo ponownie, może rozwiązać się /* SFINAE game over :( */
. Wywołanie będzie pasować
/* SFINAE operator-has-correct-sig :) */
tylko w przypadku, gdy &A::operator*
spełnia pojedynczy typ parametru tego przeciążenia, czyli E (A::*)() const
with A = T
.
Kompilator powie tutaj tak, jeśli T::operator*
ma żądany podpis, a następnie ponownie musi ocenić zwracany typ przeciążenia. Koniec z „rekurencjami” teraz: tak std::true_type
.
Jeśli kompilator nie wybierze /* SFINAE operator-exists :) */
dla wywołania test(0,0)
lub nie wybierze /* SFINAE operator-has-correct-sig :) */
dla wywołania test(&A::operator*)
, to w każdym przypadku idzie z,
/* SFINAE game over :( */
a ostatecznym typem zwracanym jest std::false_type
.
Oto program testowy, który pokazuje szablon generujący oczekiwane odpowiedzi w różnych próbkach przypadków (ponownie GCC 4.6.3).
struct empty{};
struct int_ref
{
int & operator*() const {
return *_pint;
}
int & foo() const {
return *_pint;
}
int * _pint;
};
struct sub_int_ref : int_ref{};
template<typename E>
struct ee_ref
{
E & operator*() {
return *_pe;
}
E & foo() const {
return *_pe;
}
E * _pe;
};
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;
}
Czy są jakieś nowe błędy w tym pomyśle? Czy można uczynić go bardziej ogólnym bez ponownego wpadania w zaczep, którego unika?