nodakai
nodakai

Reputation: 8011

C++ 11: overload resolution and SFINAE

I'm learning SFINAE and this is my first attempt to print "YES" only for those types you can output with std::ostream (forget about std::operator<<(std::ostream &, T) for now...):

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; }

Though they seem to work with f(std::vector<int>()) (yielding "NO,") the compiler complains f(0) is ambiguous: 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; }
      ^

How can I fix my code? Is the "YES" version not more specific than the "NO" version which is completely generic?

Clarification

All of f(0), f(0.) and f(true) fail with the same "ambiguous" error. I'm looking for a solution that is applicable to all types accepted by std::ostream::operator<<. Ideally it shouldn't rely on defining a helper type that "taints" the namespace.

Upvotes: 15

Views: 2029

Answers (2)

TartanLlama
TartanLlama

Reputation: 65620

The NO version is still valid for int and there is no applicable partial ordering to select between the two overloads, so the call is ambiguous.

One simple way of disambiguating is to add an additional tag argument to the functions:

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; }
//                ^^^

Now when you call the function, just pass an additional 0 (or write a helper function to do it for you). The SFINAE protected function will be preferred if it is valid because int is a better match than char for 0. See this article for a cleaner way of expressing this disambiguation.

Alternatively, you could write a trait to check if the operator is valid for a given type then use std::enable_if<check<T>> and std::enable_if<!check<T>> to avoid the disambiguating argument.

Incidentally, you can use decltype and trailing return types for this kind of SFINAE and I think it looks a bit cleaner:

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; }

When we get C++ Concepts, you'll be able to do something like this (this works in concepts-enabled 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; }

Upvotes: 9

Shoe
Shoe

Reputation: 76240

Since C++17 it will be possible to describe it using 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 {};

in combination with the classical 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; }

Another alternative, as recommended by @TartanLlama, would be to use std::(experimental::)is_detected as:

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

and then:

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; }

Upvotes: 5

Related Questions