Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 647
nodakai Avatar asked Aug 13 '15 09:08

nodakai


1 Answers

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; }
like image 110
TartanLlama Avatar answered Sep 27 '22 20:09

TartanLlama