4

In my code I would like to have one operation happen before another if one object is less than another. However, if the type isn't comparable it doesn't matter the order. To achieve this, I've tried to use SFINAE:

template<typename T, typename = decltype(std::declval<std::less<T>>()(std::declval<T>(), std::declval<T>()))> 
bool ComparableAndLessThan(const T& lhs, const T& rhs) {
    return std::less<T>()(lhs, rhs);
}
bool ComparableAndLessThan(...) {
    return false;
}

struct foo {};
int main()
{
    foo a,b;
    if (ComparableAndLessThan(a, b)) {
        std::cout << "a first" << std::endl;
    } else {
        std::cout << "b first" << std::endl;
    }
}

However this did not work. If I create an object without giving it a operator< or a std::less specialization, I get this error.

error C2678: binary '<': no operator found which takes a left-hand operand of type 'const foo' (or there is no acceptable conversion)
note: could be 'bool std::operator <(const std::error_condition &,const std::error_condition &) noexcept'
note: or       'bool std::operator <(const std::error_code &,const std::error_code &) noexcept'
note: while trying to match the argument list '(const foo, const foo)'
note: while compiling class template member function 'bool std::less<T>::operator ()(const _Ty &,const _Ty &) const'
       with
       [
           T=foo,
           _Ty=foo
       ]

I'm assuming because the declaration exists, SFINAE doesn't see this as an error, even though the implementation causes an error. Is there any way to check if std::less can be used on a templated type?

Jonathan Gawrych
  • 2,772
  • 23
  • 36

4 Answers4

6

The code has two issues. First is simple to fix: default template arguments are not part of overload resolution, and should not be used for SFINAE-type resolution. There is a canonical fix to it, your make a non-type template parameter of type your_decltype* and default it nullptr.

The second issue is harder. Even with fix above, SFINAE doesn't work because there is no substitute error. std::less<T> is defined for every T, it is just there is a compilation error when operator< is called. One way to solve it would be to directly use operator< for your types:

template<typename T, 
     decltype(std::declval<T>() < std::declval<T>())* = nullptr> 
bool ComparableAndLessThan(const T& lhs, const T& rhs) {
    return std::less<T>()(lhs, rhs);
}
bool ComparableAndLessThan(...) {
    return false;
}

But it's likely not want you want. I do not know how to make it work with very wildly defined std::less.

SergeyA
  • 56,524
  • 5
  • 61
  • 116
  • Okay. So I can check for `operator`. If I find it's equal to `std::less` I'll check for `operator – Jonathan Gawrych Feb 23 '18 at 18:42
  • sorry but... about the "default template arguments are not part of overload resolution, and should not be used for SFINAE-type resolution" part... are you sure? Do you mean that you can't use `template() < std::declval())>` to SFINAE purposes? – max66 Feb 23 '18 at 18:43
  • @max66 you can, but it is not a part of the function signature, and as such, does not participate in overload resolution. More details: http://en.cppreference.com/w/cpp/types/enable_if – SergeyA Feb 23 '18 at 18:46
  • @JonathanGawrych I am not saying there is no way, but I do not know of any. – SergeyA Feb 23 '18 at 18:47
  • 1
    @SergeyA - so when you write "should not be used for SFINAE-type resolution", do you mean "you can use it for SFINAE but is dangerous so is better to avoid it"? – max66 Feb 23 '18 at 18:48
  • @max66, no, I mean it will not allow you to select one of two otherwise identical overloads over another. As a result, it's best not to get into habit of using it. – SergeyA Feb 23 '18 at 21:12
3

I want to show what the issue is with a simple example.

First, SergeyA is correct that you need to make it non-type template parameter defaulted nullptr.

Now to why it doesn't work.

Because operator() of std::less is not SFINAE friendly. Let me show you what I mean:

template <class T> struct Not_working
{
    // not SFINAE friendly
    auto operator()(T a, T b) { return a < b; }
};

This is what std::less::operator() looks like. The method operator() is defined unconditionally. Even if T has a < or not, the operator() exists and has a valid declaration. The error is in the body of the function.

When you use Not_working::operator() in an SFINAE context the body of it is not an immediate context so for the purpose of SFINAE the Not_working::operator() is valid and not a failure. So the specialization is kept for the overload set and then we have an error.

In order to be usable in an SFINAE the operator() must not participate in overload resolution if there are errors in the body. In other words it itself must be SFINAEd:

template <class T> struct Working
{
    // SFINAE friendly
    template <class TT = T, decltype(TT{} < TT{})* = nullptr>
    auto operator()(T a, T b) { return a < b; }
};

Now when Working::operator() is used in an SFINAE context like in your ComparableAndLessThan then we will have an substitution failure that is not an error so SFINAE will work as expected.

In my humble opinion not making such members SFINAE friendly is an oversight of the committee but there could be some factors I am not considering.

bolov
  • 58,757
  • 13
  • 108
  • 182
0

You can re-write it like this.

template<typename T, typename F = typename std::enable_if<std::is_same<decltype(std::declval<T>() < std::declval<T>()), bool>::value, bool>::type> 
F ComparableAndLessThan(const T& lhs, const T& rhs) {
    return std::less<T>()(lhs, rhs);
}
bool ComparableAndLessThan(...) {
    return false;
}

Changed your check to instead see if decltype(std::declval<T>() < std::declval<T>()) yields a bool.

super
  • 9,722
  • 2
  • 13
  • 25
-1

SFINAE only works in the immediate context of the template. In your case, the error happens within std::less::operator() You can however first test if operator < is available; If not, check wether a specialisation of std::less is available. Have a look at this.

The Techel
  • 814
  • 8
  • 13