С++ 11: разрешение перегрузки и SFINAE

Я изучаю SFINAE, и это моя первая попытка напечатать "ДА" только для тех типов, которые вы можете выводить с помощью std::ostream (забудьте о std::operator<<(std::ostream &, T) сейчас...):

template <typename T>
void f(const T &) { std::cout << "NO" << std::endl; }

template <typename T, int SFINAE = sizeof(static_cast<std::ostream &(std::ostream::*)(T)>(
    &std::ostream::operator<<))>
void f(const T &) { std::cout << "YES" << std::endl; }

Хотя они, похоже, работают с f(std::vector<int>()) (уступая "НЕТ" ), компилятор жалуется, что f(0) неоднозначен: http://ideone.com/VljXFh

prog.cpp:16:5: error: call of overloaded 'f(int)' is ambiguous
  f(0);
     ^
prog.cpp:6:6: note: candidate: void f(const T&) [with T = int]
 void f(const T &) { std::cout << "NO" << std::endl; }
      ^
prog.cpp:10:6: note: candidate: void f(const T&) [with T = int; int SFINAE = 8]
 void f(const T &) { std::cout << "YES" << std::endl; }
      ^

Как я могу исправить свой код? Является ли версия "ДА" не более конкретной, чем версия "НЕТ", которая является полностью общей?

Разъяснение

Все f(0), f(0.) и f(true) терпят неудачу с той же "двусмысленной" ошибкой. Я ищу решение, применимое ко всем типам, принятым std::ostream::operator<<. В идеале он не должен полагаться на определение вспомогательного типа, который "заглушает" пространство имен.

Ответ 1

Версия NO по-прежнему действительна для int, и не существует соответствующего частичного заказа для выбора между двумя перегрузками, поэтому вызов неоднозначен.

Один простой способ устранения неоднозначности - добавить дополнительный аргумент тега к функциям:

template <typename T>
void f(const T &, char) { std::cout << "NO" << std::endl; }
//                ^^^^

template <typename T, int SFINAE = sizeof(static_cast<std::ostream &(std::ostream::*)(T)>(
    &std::ostream::operator<<))>
void f(const T &, int) { std::cout << "YES" << std::endl; }
//                ^^^

Теперь, когда вы вызываете функцию, просто передайте дополнительный 0 (или напишите вспомогательную функцию, чтобы сделать это для вас). Защищенная функция SFINAE будет предпочтительнее, если она действительна, потому что int лучше, чем char для 0. См. в этой статье для более чистого способа выражения этой неоднозначности.

В качестве альтернативы вы можете написать признак, чтобы проверить, действителен ли оператор для данного типа, затем используйте std::enable_if<check<T>> и std::enable_if<!check<T>>, чтобы избежать аргумента, вызывающего неоднозначность.

Кстати, вы можете использовать decltype и возвращающие возвращаемые типы для этого вида SFINAE, и я думаю, что он выглядит немного чище:

template <typename T>
void f(const T &, char) { std::cout << "NO" << std::endl; }

template <typename T>
auto f(const T &t, int) -> decltype(std::declval<std::ostream&>() << t, void())
{ std::cout << "YES" << std::endl; }

Когда мы получим С++ Concepts, вы сможете сделать что-то вроде этого (это работает в GCC с включенной концепцией):

template <typename T>
concept bool Outputtable = requires (T t, std::ostream o) { o << t; };

template <typename T>
void f(const T &) { std::cout << "NO" << std::endl; }

template <Outputtable T>
void f(const T &) { std::cout << "YES" << std::endl; }

Ответ 2

Поскольку С++ 17 можно будет описать с помощью std::void_t:

template<typename, typename = std::void_t<>>
struct enables_ostream_output : std::false_type {};

template<typename Type>
struct enables_ostream_output<
    Type,
    std::void_t<decltype(std::declval<std::ostream>() << std::declval<Type>())>
> : std::true_type {};

в сочетании с классическим std::enable_if:

template <typename Type>
typename std::enable_if<!enables_ostream_output<Type>::value>::type
f(const Type &) { std::cout << "NO" << std::endl; }

template <typename Type>
typename std::enable_if<enables_ostream_output<Type>::value>::type
f(const Type &) { std::cout << "YES" << std::endl; }

Другой вариант, рекомендованный @TartanLlama, заключается в использовании std::(experimental::)is_detected как:

template<typename Type>
using ostream_output_t = decltype(std::declval<std::ostream>() << std::declval<Type>());

а затем:

template <typename Type>
typename std::enable_if<!std::is_detected<ostream_output_t, Type>::value>::type
f(const Type &) { std::cout << "NO" << std::endl; }

template <typename Type>
typename std::enable_if<std::is_detected<ostream_output_t, Type>::value>::type
f(const Type &) { std::cout << "YES" << std::endl; }