Прийнята відповідь на це запитання про інтроспекцію членів функції компіляції, хоч і є просто популярною, має затяжку, яку можна спостерігати в наступній програмі:
#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;
}
Побудований з GCC 4.6.3, програма виводить 110
- інформують про те , що
T = std::shared_ptr<int>
це НЕ забезпечить int & T::operator*() const
.
Якщо ви вже не мудрі до цієї ґотчі, тоді погляд на визначення
std::shared_ptr<T>
в заголовку <memory>
прожене світло. У цій реалізації std::shared_ptr<T>
походить від базового класу, з якого він успадковує operator*() const
. Таким чином, шаблонна інстанція,
SFINAE<U, &U::operator*>
яка становить "пошук" оператора для
U = std::shared_ptr<T>
, не відбудеться, тому std::shared_ptr<T>
що не має
operator*()
власного права, а екземпляр шаблону не "робить спадщину".
Цей корч не впливає на добре відомий підхід SFINAE, використовуючи "Tirke sizeof ()", для виявлення того, чи T
є якась функція члена mf
(див., Наприклад,
цю відповідь та коментарі). Але встановлення того, що T::mf
існує, часто (зазвичай?) Недостатньо хороше: вам також може знадобитися встановити, що він має бажаний підпис. Саме тут і зазначається ілюстрована техніка. Вказівний варіант потрібного підпису вписаний у параметр типу шаблону, який повинен бути задоволений
&T::mf
для успіху зондом SFINAE. Але цей метод інстантування шаблону дає неправильну відповідь, колиT::mf
він передається у спадок.
Безпечна техніка SFINAE для інтроспекції компіляції за компіляцією T::mf
повинна уникати використання&T::mf
в аргументі шаблону для встановлення типу, від якого залежить роздільна здатність шаблону функції SFINAE. Натомість роздільна здатність функції шаблону SFINAE може залежати лише від точно відповідних оголошень типу, що використовуються як типи аргументів перевантаженої функції зонда SFINAE.
У відповідь на питання, яке дотримується цього обмеження, я проілюструю для виявлення під час компіляції E T::operator*() const
, для довільного T
та E
. Цей же шаблон буде застосовано mutatis mutandis
для зондування для будь-якого іншого підпису методу-члена.
#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? */
};
У цьому рішенні функція зонду SFINAE test()
"викликається рекурсивно". (Звичайно, насправді це взагалі не викликається; у нього є лише відповідні типи гіпотетичних викликів, вирішені компілятором.)
Нам потрібно дослідити щонайменше одну та максимум дві точки інформації:
- Чи
T::operator*()
існує взагалі? Якщо ні, ми закінчили.
- З огляду на те, що
T::operator*()
існує, є його підписом
E T::operator*() const
?
Відповіді ми отримуємо, оцінюючи тип повернення одного дзвінка до test(0,0)
. Це зроблено:
typedef decltype(test<T>(0,0)) type;
Цей виклик може бути вирішений до /* SFINAE operator-exists :) */
перевантаження test()
, або він може вирішити /* SFINAE game over :( */
перевантаження. Він не може вирішити /* SFINAE operator-has-correct-sig :) */
перевантаження, тому що очікує лише одного аргументу, і ми передаємо два.
Чому ми проїжджаємо два? Просто змусити резолюцію виключити
/* SFINAE operator-has-correct-sig :) */
. Другий аргумент не має іншого значення.
Цей заклик до test(0,0)
вирішиться /* SFINAE operator-exists :) */
лише на випадок, коли перший аргумент 0 задовольняє тип першого параметра тієї перевантаження, що є decltype(&A::operator*)
, з A = T
. 0 задовольнить цей тип на всякий випадок, якщо T::operator*
існує.
Припустимо, компілятор скаже "Так". Потім це відбувається,
/* SFINAE operator-exists :) */
і йому потрібно визначити тип повернення функціонального виклику, який у такому випадку decltype(test(&A::operator*))
- тип повернення ще одного виклику test()
.
Цього разу ми передаємо лише один аргумент, &A::operator*
який, як ми зараз знаємо, існує, інакше нас би тут не було. Виклик test(&A::operator*)
може вирішити або до, /* SFINAE operator-has-correct-sig :) */
і знову, щоб вирішити /* SFINAE game over :( */
. Виклик буде відповідати
/* SFINAE operator-has-correct-sig :) */
тільки в разі , якщо &A::operator*
задовольняє єдиний параметр типу тієї перевантаження, яка E (A::*)() const
, з A = T
.
Тут компілятор скаже "Так", якщо T::operator*
має бажаний підпис, а потім знову повинен оцінити тип повернення перевантаження. Зараз більше немає "рекурсій": це так std::true_type
.
Якщо компілятор не вибирає /* SFINAE operator-exists :) */
дзвінок test(0,0)
або не вибирає /* SFINAE operator-has-correct-sig :) */
для виклику test(&A::operator*)
, то в будь-якому випадку це стосується
/* SFINAE game over :( */
і остаточного типу повернення std::false_type
.
Ось тестова програма, яка показує шаблон, що дає очікувані відповіді у різноманітній вибірці випадків (знову GCC 4.6.3).
// 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;
}
Чи є в цій ідеї нові вади? Чи можна зробити це більш загальним, не повторивши при цьому зламу, який він уникає?