Принятый ответ на этот вопрос об интроспекции функций-членов во время компиляции, хотя он и пользуется заслуженной популярностью, имеет загвоздку, которую можно наблюдать в следующей программе:
#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, использующий «Уловку 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*)
with 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;
}
Есть ли у этой идеи новые недостатки? Можно ли сделать его более общим, чтобы в очередной раз не попасть в ловушку, которую он избегает?