C++ 11: overload resolution and SFINAE
Asked Answered
S

2

15

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.

Shreeves answered 13/8, 2015 at 9:36 Comment(0)
B
9

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; }
Berck answered 13/8, 2015 at 9:42 Comment(5)
For tagging, I would prefer to use something like struct overload_priority_high : overload_priority_low {}; which is more readable than int vs char or long.Windsail
@Windsail I agree, which is why I linked to Xeo's article on the subject. I could edit some extra stuff in, but the answer is already getting a bit long and tangential!Berck
Your solution of adding another parameter seems too specific to char vs int. In fact I get "ambiguous" errors with f(0.) and f(true) as well. Otherwise is it mandatory to define a trait? I'd prefer "one-liner" solution if possible.Shreeves
As I suggested, it's best to write a simple forwarding function to hide the disambiguating argument from client code.Berck
Or do something like Jarod42 suggested or something from the article I linked to.Berck
A
5

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; }
Alla answered 13/8, 2015 at 10:1 Comment(5)
You could probably get an even nicer version out of std::is_detected or Concepts.Berck
@Berck :(Alla
Well, currently std::experimental::is_detectedBerck
Is it mandatory to explicitly disable the "NO" version with enable_if< ! ...> ?Shreeves
@Shreeves Yes, otherwise it's ambiguous.Alla

© 2022 - 2024 — McMap. All rights reserved.