Почему SFINAE не работает в правой части аргументов функции по умолчанию?
У меня есть этот код:
struct My
{
typedef int foo;
};
struct My2
{
};
template <typename T>
void Bar(const T&, int z = typename T::foo())
{
std::cout << "My" << std::endl;
}
void Bar(...)
{
std::cout << "..." << std::endl;
}
int main()
{
My my;
Bar(my); // OK
My2 my2;
Bar(my2); // Compile error: no type named ‘foo’ in ‘struct My2’
return 0;
}
Я полагаю, что если какой-либо класс T не имеет typedef foo внутри, компилятор должен исключить первую перегрузку и выбрать перегрузку с помощью многоточия. Но я проверяю этот код на MSVC, gcc и clang, и я получаю ошибку компиляции для этих компиляторов. Почему SFINAE не работает в этом случае?
Ответы
Ответ 1
Тип z
не подлежит замене шаблона, он всегда int
. Это означает, что для SFINAE нет возможности, и вместо этого вы получите ошибку компилятора при попытке разрешить T::foo
значение по умолчанию. Аргументы по умолчанию не участвуют в разрешении перегрузки, а создаются только тогда, когда они отсутствуют в вызове функции. Раздел 14.7.1 (параграфы 13/14) стандарта описывает это поведение, но не дает смысла в отсутствии SFINAE здесь.
SFINAE можно разрешить, установив тип z
параметра шаблона, как показано ниже:
(живой пример: http://ideone.com/JynMye)
#include <iostream>
struct My
{
typedef int foo;
};
struct My2
{
};
template<typename T, typename I=typename T::foo> void Bar(const T&, I z = I())
{
std::cout << "My\n";
}
void Bar(...)
{
std::cout << "...\n";
}
int main()
{
My my;
Bar(my); // OK
My2 my2;
Bar(my2); // Also OK
return 0;
}
Это будет использовать "Моя" версия для первого вызова и "..." для второго вызова. Выходной сигнал
My
...
Однако, если void Bar (...) был шаблоном, по какой-то причине версия "Моя" никогда не получит шанс:
(живой пример: http://ideone.com/xBQiIh)
#include <iostream>
struct My
{
typedef int foo;
};
struct My2
{
};
template<typename T, typename I=typename T::foo> void Bar(const T&, I z = I())
{
std::cout << "My\n";
}
template<typename T> void Bar(T&)
{
std::cout << "...\n";
}
int main()
{
My my;
Bar(my); // OK
My2 my2;
Bar(my2); // Also OK
return 0;
}
Здесь в обоих случаях вызывается версия "..." . Выход:
...
...
Одним из решений является использование класса (частичная) специализация; укажите "..." в качестве базы, тип второго параметра по умолчанию - int
, а "Моя" - как специализация, где второй параметр typename T::foo
. В сочетании с простой функцией шаблона для вывода T и отправки в соответствующую функцию-член класса это дает желаемый эффект:
(живой пример: http://ideone.com/FanLPc)
#include <iostream>
struct My
{
typedef int foo;
};
struct My2
{
};
template<typename T, typename I=int> struct call_traits {
static void Bar(...)
{
std::cout << "...\n";
}
};
template<typename T> struct call_traits<T, typename T::foo> {
static void Bar(const T&, int z=typename T::foo())
{
std::cout << "My\n";
}
};
template<typename T> void Bar(const T& t)
{
call_traits<T>::Bar(t);
}
int main()
{
My my;
Bar(my); // OK
My2 my2;
Bar(my2); // Still OK
return 0;
}
Здесь вывод:
My
...
Ответ 2
Тип z
является int
, не выводится компилятором, и для SFINAE не должно быть места. Значение, используемое для инициализации z
, основано на стандарте T::foo
, которого не существует; следовательно, ошибка.
Если тип для z
повышен до самого шаблона, замена теперь может завершиться неудачей, а SFINAE начнет работать.
#include <iostream>
struct My
{
typedef int foo;
};
struct My2
{
};
template <typename T, typename I = typename T::foo>
void Bar(const T&, I z = I())
{
(void)z; // silence any warnings on unused
std::cout << "My" << std::endl;
}
void Bar(...)
{
std::cout << "..." << std::endl;
}
int main()
{
My my;
Bar(my);
My2 my2;
Bar(my2); // Compiles
return 0;
}
Живой пример
Чтобы шаблон функции был частью перегруженного списка функций-кандидатов, вывод аргумента шаблона должен быть успешным. Если это не удается, кандидат удаляется из списка. Следовательно, если не происходит сбой дедукции, он добавляется в список кандидатов (но это не исключает дальнейших ошибок, если он окончательно выбран).
14.8.3/1 Разрешение перегрузки
Функциональный шаблон может быть перегружен либо (не шаблонными) функциями его имени, либо (другими) шаблонами функций с тем же именем. Когда вызов этого имени записывается (явно или неявно с использованием операторной нотации), вывод аргумента шаблона (14.8.2) и проверка любых явных аргументов шаблона (14.3) выполняются для каждого шаблона функции для поиска значений аргумента шаблона ( если таковые имеются), которые можно использовать с этим шаблоном функции, чтобы создать экземпляр специализированной функции, которая может быть вызвана с помощью аргументов вызова. Для каждого шаблона функции, если вывод аргументов и проверка завершаются успешно, аргументы шаблона (выводимые и/или явные) используются для синтеза декларации одной специализированной функции шаблона, которая добавляется к кандидатам, установленным для использования при разрешении перегрузки. Если для данного шаблона функции завершается вывод аргумента аргумента, эта функция не добавляется к набору функций-кандидатов для этого шаблона. Полный набор функций-кандидатов включает в себя все синтезированные объявления и все нестратегированные перегруженные функции с тем же именем. Синтезированные объявления обрабатываются как любые другие функции в остальной части разрешения перегрузки, за исключением случаев, явно указанных в 13.3.3.
Вывод аргумента шаблона выполняется по типу функции и самим аргументам шаблона.
14.8.2/8 Вывод аргумента шаблона
Если подстановка приводит к недопустимому типу или выражению, тип дедукции не выполняется. Недопустимым типом или выражением является тот, который был бы плохо сформирован, с требуемой диагностикой, если он написан с использованием замещенных аргументов. [Примечание: если диагностика не требуется, программа все еще плохо сформирована. Проверка доступа выполняется как часть процесса замещения. -end note] Только недопустимые типы и выражения в немедленном контексте типа функции и его типах параметров шаблона могут привести к ошибке дедукции.
От OP, функция Bar<T>
добавляется в список кандидатов, так как можно вывести, что такое тип для T
. Он создается и аргументы по умолчанию проверяются, и, следовательно, он терпит неудачу.
14.7.1/13 Неявное создание экземпляра
Если шаблон функции f вызывается так, что требуется использовать аргумент по умолчанию, зависимые имена просматриваются, проверяются ограничения семантики, а создание экземпляра любого шаблона, используемого в аргументе по умолчанию, сделано так, как будто аргумент по умолчанию был инициализатором, используемым в специализации шаблона функции с той же областью, теми же параметрами шаблона и тем же доступом, что и шаблон функции f
, используемый в этой точке. Этот анализ называется созданием аргументов по умолчанию. Конкретный аргумент по умолчанию затем используется как аргумент f
.
Цитаты взяты из черновик n3797
Ответ 3
Еще один совместимый с С++ 03 вариант. Поскольку в ответах выше аргумент по умолчанию использовался в функции шаблона, и он не разрешен в стандарте.
#include <iostream>
struct TypeWithFoo{
typedef int Foo;
};
template<typename T, bool>
struct onFooAction;
template<typename T>
struct onFooAction<T, false>{
void operator ()(const T &t){
std::cout << "No foo :(\n";
}
};
template<typename T>
struct onFooAction<T, true>{
void operator ()(const T &t){
std::cout << "Foo =)\n";
}
};
template<typename T>
struct hasFoo{
typedef char yes[1];
typedef char no[2];
template<typename C>
static yes& testForFoo(typename C::Foo*);
template<typename>
static no& testForFoo(...);
static const bool value = sizeof(testForFoo<T>(0)) == sizeof(yes);
};
template<typename T>
void bar(const T &t){
onFooAction<T, hasFoo<T>::value>()(t);
}
int main(){
bar(10);
bar(TypeWithFoo());
}