Почему 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());
}